diff --git a/.changeset/actions-permissions-api.md b/.changeset/actions-permissions-api.md new file mode 100644 index 0000000000..c38099a8a9 --- /dev/null +++ b/.changeset/actions-permissions-api.md @@ -0,0 +1,30 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +Added optional `visibilityPermission` field to `ActionsRegistryActionOptions`, allowing actions to declare a `BasicPermission` that controls visibility and access. + +```typescript +import { createPermission } from '@backstage/plugin-permission-common'; + +const myPermission = createPermission({ + name: 'myPlugin.myAction.use', + attributes: {}, +}); + +actionsRegistry.register({ + name: 'my-action', + title: 'My Action', + description: 'An action that requires permission', + visibilityPermission: myPermission, + schema: { + input: z => z.object({ name: z.string() }), + output: z => z.object({ ok: z.boolean() }), + }, + action: async ({ input }) => { + return { output: { ok: true } }; + }, +}); +``` + +Actions without a `visibilityPermission` field continue to work as before. diff --git a/.changeset/actions-permissions-defaults.md b/.changeset/actions-permissions-defaults.md new file mode 100644 index 0000000000..66d931fa6d --- /dev/null +++ b/.changeset/actions-permissions-defaults.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added permissions integration to the actions registry. Actions registered with a `visibilityPermission` field are now checked against the permissions framework when listing and invoking. Denied actions are filtered from list results, and invoking a denied action returns a `404 Not Found` as if the action does not exist. Permissions are automatically registered with the `PermissionsRegistryService` so they appear in the permission policy system. diff --git a/.changeset/actions-service-plugin-id.md b/.changeset/actions-service-plugin-id.md new file mode 100644 index 0000000000..3fc86db8d8 --- /dev/null +++ b/.changeset/actions-service-plugin-id.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +'@backstage/backend-test-utils': patch +--- + +Added `pluginId` field to `ActionsServiceAction` type, populated from the registering plugin's metadata. diff --git a/.changeset/add-cli-plugin-role.md b/.changeset/add-cli-plugin-role.md new file mode 100644 index 0000000000..1f09a08ade --- /dev/null +++ b/.changeset/add-cli-plugin-role.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added a new `cli-module` package role for packages that provide CLI plugin extensions. diff --git a/.changeset/add-frontend-dev-utils.md b/.changeset/add-frontend-dev-utils.md new file mode 100644 index 0000000000..987f02c6d9 --- /dev/null +++ b/.changeset/add-frontend-dev-utils.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-dev-utils': minor +--- + +Added `@backstage/frontend-dev-utils`, a new package that provides a minimal helper for wiring up a development app for frontend plugins using the new frontend system. It exports a `createDevApp` function that handles creating and rendering a development app from a `dev/` entry point. The dev app automatically bypasses the sign-in page and loads the `@backstage/ui` CSS. The options interface accepts `features` together with route bindings through `bindRoutes`. diff --git a/.changeset/add-listbox-component.md b/.changeset/add-listbox-component.md new file mode 100644 index 0000000000..4c510e61f9 --- /dev/null +++ b/.changeset/add-listbox-component.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added `List` and `ListRow` components. These provide a standalone, accessible list of interactive rows built on top of React Aria's `GridList` and `GridListItem` primitives. Rows support icons, descriptions, actions, menus, and single or multiple selection modes. + +**Affected components:** List, ListRow diff --git a/.changeset/add-masked-and-hidden-option.md b/.changeset/add-masked-and-hidden-option.md new file mode 100644 index 0000000000..ad5c810a8e --- /dev/null +++ b/.changeset/add-masked-and-hidden-option.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added `maskedAndHidden` option to `gitlab:projectVariable:create` and `publish:gitlab` action to support creating GitLab project variables that are both masked and hidden. Updated gitbeaker to version 43.8.0 for proper type support. diff --git a/.changeset/add-query-catalog-entities-action.md b/.changeset/add-query-catalog-entities-action.md new file mode 100644 index 0000000000..78fb30d584 --- /dev/null +++ b/.changeset/add-query-catalog-entities-action.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/add-scheduler-cancel-task-test-utils.md b/.changeset/add-scheduler-cancel-task-test-utils.md new file mode 100644 index 0000000000..242b103eef --- /dev/null +++ b/.changeset/add-scheduler-cancel-task-test-utils.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added `cancelTask` to `MockSchedulerService` and mock scheduler service factory. diff --git a/.changeset/add-scheduler-cancel-task.md b/.changeset/add-scheduler-cancel-task.md new file mode 100644 index 0000000000..1f98bccb1c --- /dev/null +++ b/.changeset/add-scheduler-cancel-task.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': minor +'@backstage/backend-defaults': patch +--- + +Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available. diff --git a/.changeset/add-who-am-i-action.md b/.changeset/add-who-am-i-action.md new file mode 100644 index 0000000000..5c1008a7e1 --- /dev/null +++ b/.changeset/add-who-am-i-action.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user. diff --git a/.changeset/add-withApis-core-compat-api.md b/.changeset/add-withApis-core-compat-api.md new file mode 100644 index 0000000000..135dcf87c7 --- /dev/null +++ b/.changeset/add-withApis-core-compat-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Added `withApis`, which is a Higher-Order Component for providing APIs as props to a component via `useApiHolder`. diff --git a/.changeset/afraid-rats-invent.md b/.changeset/afraid-rats-invent.md deleted file mode 100644 index babf0e9d7a..0000000000 --- a/.changeset/afraid-rats-invent.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Added a new `FullPage` component that fills the remaining viewport height below the `PluginHeader`. - -```tsx - - - {/* content fills remaining height */} - -``` - -**Affected components:** FullPage diff --git a/.changeset/alert-remove-surface.md b/.changeset/alert-remove-surface.md deleted file mode 100644 index fcdd9303e5..0000000000 --- a/.changeset/alert-remove-surface.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Alert no longer accepts a `surface` prop - -The Alert component's background is now driven entirely by its `status` prop. The `surface` prop has been removed. - -```diff -- -+ -``` - -**Affected components:** Alert diff --git a/.changeset/angry-hornets-clap.md b/.changeset/angry-hornets-clap.md deleted file mode 100644 index bd590e71d9..0000000000 --- a/.changeset/angry-hornets-clap.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Removed gray scale tokens and renamed background surface tokens to neutral tokens - -The `--bui-gray-1` through `--bui-gray-8` tokens have been removed. The `--bui-bg-surface-*` and `--bui-bg-neutral-on-surface-*` tokens have been replaced by a unified `--bui-bg-neutral-*` scale. - -**Migration:** - -Replace surface tokens directly: - -```diff -- background: var(--bui-bg-surface-0); -+ background: var(--bui-bg-neutral-0); -``` - -Replace on-surface tokens shifted by +1: - -```diff -- background: var(--bui-bg-neutral-on-surface-0); -+ background: var(--bui-bg-neutral-1); -``` - -Replace gray tokens 1-4 with neutral equivalents (`--bui-gray-5` through `--bui-gray-8` have no direct replacement): - -```diff -- background: var(--bui-gray-1); -+ background: var(--bui-bg-neutral-1); -``` diff --git a/.changeset/api-override-conflict-error-defaults.md b/.changeset/api-override-conflict-error-defaults.md deleted file mode 100644 index 703f76fb6e..0000000000 --- a/.changeset/api-override-conflict-error-defaults.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-defaults': minor ---- - -**BREAKING**: The `API_FACTORY_CONFLICT` warning is now treated as an error and will prevent the app from starting. diff --git a/.changeset/api-override-conflict-error.md b/.changeset/api-override-conflict-error.md deleted file mode 100644 index ed88452ccf..0000000000 --- a/.changeset/api-override-conflict-error.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-app-api': minor ---- - -**BREAKING**: Updated the behavior of the new API override logic to reject the override and block app startup instead of just logging a deprecation warning. diff --git a/.changeset/api-override-test-utils.md b/.changeset/api-override-test-utils.md deleted file mode 100644 index 6ea60f9e1b..0000000000 --- a/.changeset/api-override-test-utils.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@backstage/frontend-test-utils': patch ---- - -Added an `apis` option to `createExtensionTester`, `renderInTestApp`, and `renderTestApp` to override APIs when testing extensions. Use the `mockApis` helpers to create mock implementations: - -```typescript -import { identityApiRef } from '@backstage/frontend-plugin-api'; -import { mockApis } from '@backstage/frontend-test-utils'; - -// Override APIs in createExtensionTester -const tester = createExtensionTester(myExtension, { - apis: [ - [ - identityApiRef, - mockApis.identity({ userEntityRef: 'user:default/guest' }), - ], - ], -}); - -// Override APIs in renderInTestApp -renderInTestApp(, { - apis: [ - [ - identityApiRef, - mockApis.identity({ userEntityRef: 'user:default/guest' }), - ], - ], -}); - -// Override APIs in renderTestApp -renderTestApp({ - extensions: [myExtension], - apis: [ - [ - identityApiRef, - mockApis.identity({ userEntityRef: 'user:default/guest' }), - ], - ], -}); -``` diff --git a/.changeset/app-visualizer-use-frontend-dev-utils.md b/.changeset/app-visualizer-use-frontend-dev-utils.md new file mode 100644 index 0000000000..f047fe8f7e --- /dev/null +++ b/.changeset/app-visualizer-use-frontend-dev-utils.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': patch +--- + +Switched dev entry point to use `createDevApp` from `@backstage/frontend-dev-utils`. diff --git a/.changeset/auth-backend-cimd-endpoint.md b/.changeset/auth-backend-cimd-endpoint.md new file mode 100644 index 0000000000..5e2fd6761a --- /dev/null +++ b/.changeset/auth-backend-cimd-endpoint.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added optional client metadata document endpoint at `/.well-known/oauth-client/cli.json` relative to the auth backend base URL for CLI authentication. Enabled when `auth.experimentalClientIdMetadataDocuments.enabled` is set to `true`. diff --git a/.changeset/auth-migrate-to-bui.md b/.changeset/auth-migrate-to-bui.md new file mode 100644 index 0000000000..974fe5abee --- /dev/null +++ b/.changeset/auth-migrate-to-bui.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth': patch +--- + +Migrated the ConsentPage UI from Material-UI and `@backstage/core-components` to `@backstage/ui`. diff --git a/.changeset/auth-provider-icon-element-core-components.md b/.changeset/auth-provider-icon-element-core-components.md new file mode 100644 index 0000000000..7bd7aa6d52 --- /dev/null +++ b/.changeset/auth-provider-icon-element-core-components.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +The login request dialog now handles auth provider icons passed as `IconElement` in addition to `IconComponent`. diff --git a/.changeset/auth-provider-icon-element-user-settings.md b/.changeset/auth-provider-icon-element-user-settings.md new file mode 100644 index 0000000000..16b9adb94f --- /dev/null +++ b/.changeset/auth-provider-icon-element-user-settings.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +The `ProviderSettingsItem` `icon` prop now accepts `IconElement` in addition to `IconComponent`. diff --git a/.changeset/auth-provider-icon-element.md b/.changeset/auth-provider-icon-element.md new file mode 100644 index 0000000000..def6666c2d --- /dev/null +++ b/.changeset/auth-provider-icon-element.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +The `icon` field on `AuthProviderInfo` now accepts `IconElement` in addition to `IconComponent`, letting you pass `` instead of `MyIcon`. diff --git a/.changeset/backend-process-listener-cleanup.md b/.changeset/backend-process-listener-cleanup.md deleted file mode 100644 index eb4dc52937..0000000000 --- a/.changeset/backend-process-listener-cleanup.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Fixed memory leak by properly cleaning up process event listeners on backend shutdown. diff --git a/.changeset/backend-test-utils-create-service-mock.md b/.changeset/backend-test-utils-create-service-mock.md deleted file mode 100644 index b35b647fd8..0000000000 --- a/.changeset/backend-test-utils-create-service-mock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': minor ---- - -Added `createServiceMock`, a public utility for creating `ServiceMock` instances for custom service refs. This allows plugin authors to define mock creators for their own services following the same pattern as the built-in `mockServices` mocks. diff --git a/.changeset/backend-test-utils-extension-points.md b/.changeset/backend-test-utils-extension-points.md deleted file mode 100644 index fb26a282e3..0000000000 --- a/.changeset/backend-test-utils-extension-points.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Updated `startTestBackend` to support factory-based extension points (v1.1 format) in addition to the existing direct implementation format. diff --git a/.changeset/beige-crabs-share.md b/.changeset/beige-crabs-share.md deleted file mode 100644 index 6a1a42ffd8..0000000000 --- a/.changeset/beige-crabs-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-test-utils': minor ---- - -**BREAKING**: Removed the `TestApiRegistry` class, use `TestApiProvider` directly instead, storing reused APIs in a variable, e.g. `const apis = [...] as const`. diff --git a/.changeset/better-eggs-slide.md b/.changeset/better-eggs-slide.md deleted file mode 100644 index cf3856bb86..0000000000 --- a/.changeset/better-eggs-slide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-gitlab': patch ---- - -Allow setting optional description on group creation diff --git a/.changeset/big-rabbits-think.md b/.changeset/big-rabbits-think.md deleted file mode 100644 index bd0ae32b5e..0000000000 --- a/.changeset/big-rabbits-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Fixes a bug where the `EntityListProvider` would not correctly hydrate query parameters if more than 20 were provided for the same key. diff --git a/.changeset/blue-moons-crash.md b/.changeset/blue-moons-crash.md new file mode 100644 index 0000000000..bb5ae15c2e --- /dev/null +++ b/.changeset/blue-moons-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components. diff --git a/.changeset/brave-pens-argue.md b/.changeset/brave-pens-argue.md new file mode 100644 index 0000000000..2ae66c29cf --- /dev/null +++ b/.changeset/brave-pens-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +--- + +Fixed `resolvePackagePath` resolution for bundled dynamic plugins. When a plugin bundles its own copy of `@backstage/backend-plugin-api` inside `node_modules`, the `CommonJSModuleLoader` fallback now correctly resolves the plugin's `package.json` by name. Previously the fallback only applied when the resolution originated from the host application; it now also applies when originating from a bundled dependency, which is the case for plugins produced by the `backstage-cli package bundle` command. diff --git a/.changeset/bright-items-see.md b/.changeset/bright-items-see.md new file mode 100644 index 0000000000..ea3ba17cdf --- /dev/null +++ b/.changeset/bright-items-see.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. diff --git a/.changeset/bright-moons-open.md b/.changeset/bright-moons-open.md new file mode 100644 index 0000000000..2668dfb2e6 --- /dev/null +++ b/.changeset/bright-moons-open.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Fixes the search component not registering the first search on navigate to the search page. diff --git a/.changeset/bright-pans-greet.md b/.changeset/bright-pans-greet.md deleted file mode 100644 index 89ac3a8bc6..0000000000 --- a/.changeset/bright-pans-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-visualizer': patch ---- - -Bump react-aria-components to v1.14.0 diff --git a/.changeset/brown-grapes-fold.md b/.changeset/brown-grapes-fold.md deleted file mode 100644 index d8ab0a23f3..0000000000 --- a/.changeset/brown-grapes-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-auth0-provider': patch ---- - -Add support for organizational invites in auth0 strategy diff --git a/.changeset/brown-rings-sort.md b/.changeset/brown-rings-sort.md new file mode 100644 index 0000000000..045f2fbd3b --- /dev/null +++ b/.changeset/brown-rings-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added interactive support to the `Card` component. Pass `onPress` to make the entire card surface pressable, or `href` to make it navigate to a URL. A transparent overlay handles the interaction while nested buttons and links remain independently clickable. diff --git a/.changeset/brown-towns-find.md b/.changeset/brown-towns-find.md new file mode 100644 index 0000000000..c3244d9427 --- /dev/null +++ b/.changeset/brown-towns-find.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Fixed invisible text in parameter input fields when using dark mode in OpenAPI definition pages diff --git a/.changeset/bui-analytics.md b/.changeset/bui-analytics.md new file mode 100644 index 0000000000..65ba31b1cf --- /dev/null +++ b/.changeset/bui-analytics.md @@ -0,0 +1,11 @@ +--- +'@backstage/ui': patch +--- + +Added analytics capabilities to the component library. Components with navigation behavior (Link, ButtonLink, Tab, MenuItem, Tag, Row) now fire analytics events on click when a `BUIProvider` is present. + +New exports: `BUIProvider`, `useAnalytics`, `getNodeText`, and associated types (`AnalyticsTracker`, `UseAnalyticsFn`, `BUIProviderProps`, `AnalyticsEventAttributes`). + +Components with analytics support now accept a `noTrack` prop to suppress event firing. + +**Affected components:** Link, ButtonLink, Tab, MenuItem, Tag, Row diff --git a/.changeset/bui-table-root-loading-prop.md b/.changeset/bui-table-root-loading-prop.md new file mode 100644 index 0000000000..7c3f453c82 --- /dev/null +++ b/.changeset/bui-table-root-loading-prop.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added a `loading` prop and `data-loading` data attribute to `TableRoot`, allowing consumers to distinguish between stale data and initial loading states. Both `stale` and `loading` set `aria-busy` on the table. + +Affected components: TableRoot diff --git a/.changeset/bui-table-skeleton-loading.md b/.changeset/bui-table-skeleton-loading.md new file mode 100644 index 0000000000..df20393c50 --- /dev/null +++ b/.changeset/bui-table-skeleton-loading.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Improved the `Table` component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes proper accessibility support with `aria-busy` on the table and screen reader announcements. + +Affected components: Table diff --git a/.changeset/bump-bfj-v9.md b/.changeset/bump-bfj-v9.md new file mode 100644 index 0000000000..63309e4657 --- /dev/null +++ b/.changeset/bump-bfj-v9.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `bfj` to `^9.0.2`. diff --git a/.changeset/bumpy-colts-teach.md b/.changeset/bumpy-colts-teach.md new file mode 100644 index 0000000000..e3457127ce --- /dev/null +++ b/.changeset/bumpy-colts-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed neutral-1 hover & pressed state in light mode. diff --git a/.changeset/bumpy-keys-pay.md b/.changeset/bumpy-keys-pay.md new file mode 100644 index 0000000000..fdd698fd7b --- /dev/null +++ b/.changeset/bumpy-keys-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added new `gitlab:group:access` scaffolder action to add or remove users and groups as members of GitLab groups. The action supports specifying members via `userIds` and/or `groupIds` array parameters, configurable access levels (Guest, Reporter, Developer, Maintainer, Owner), and defaults to the 'add' action when not specified. diff --git a/.changeset/by-refs-predicate-backend.md b/.changeset/by-refs-predicate-backend.md new file mode 100644 index 0000000000..a2a0346122 --- /dev/null +++ b/.changeset/by-refs-predicate-backend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/by-refs-predicate-client.md b/.changeset/by-refs-predicate-client.md new file mode 100644 index 0000000000..5ed136fbe1 --- /dev/null +++ b/.changeset/by-refs-predicate-client.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/calm-knives-glow.md b/.changeset/calm-knives-glow.md deleted file mode 100644 index f2e1d5805c..0000000000 --- a/.changeset/calm-knives-glow.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/integration': patch -'@backstage/plugin-catalog-backend-module-azure': patch ---- - -Added support for `{org}.visualstudio.com` domains used by Azure DevOps diff --git a/.changeset/calm-vans-play.md b/.changeset/calm-vans-play.md new file mode 100644 index 0000000000..0b7571da7b --- /dev/null +++ b/.changeset/calm-vans-play.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + +**Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden diff --git a/.changeset/card-hybrid-click.md b/.changeset/card-hybrid-click.md new file mode 100644 index 0000000000..48343c72a7 --- /dev/null +++ b/.changeset/card-hybrid-click.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed interactive cards so that CardBody can scroll when the card has a constrained height. Previously, the overlay element blocked scroll events. + +**Affected components:** Card diff --git a/.changeset/catalog-graph-test-deps.md b/.changeset/catalog-graph-test-deps.md deleted file mode 100644 index 3c47695696..0000000000 --- a/.changeset/catalog-graph-test-deps.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-org': patch ---- - -Added `@backstage/frontend-test-utils` dev dependency. diff --git a/.changeset/catalog-modules-update-extension-points.md b/.changeset/catalog-modules-update-extension-points.md deleted file mode 100644 index dba229482f..0000000000 --- a/.changeset/catalog-modules-update-extension-points.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/plugin-catalog-backend-module-azure': patch -'@backstage/plugin-catalog-backend-module-backstage-openapi': patch -'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch -'@backstage/plugin-catalog-backend-module-gcp': patch -'@backstage/plugin-catalog-backend-module-gerrit': patch -'@backstage/plugin-catalog-backend-module-gitea': patch -'@backstage/plugin-catalog-backend-module-github': patch -'@backstage/plugin-catalog-backend-module-github-org': patch -'@backstage/plugin-catalog-backend-module-gitlab': patch -'@backstage/plugin-catalog-backend-module-gitlab-org': patch -'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-catalog-backend-module-openapi': patch -'@backstage/plugin-catalog-backend-module-puppetdb': patch -'@backstage/plugin-catalog-backend-module-scaffolder-entity-model': patch ---- - -Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. diff --git a/.changeset/catalog-node-extension-points-to-stable.md b/.changeset/catalog-node-extension-points-to-stable.md deleted file mode 100644 index 7e8e22dfc5..0000000000 --- a/.changeset/catalog-node-extension-points-to-stable.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-catalog-node': minor ---- - -Promoted stable catalog extension points from alpha to main export. The following extension points are now exported from `@backstage/plugin-catalog-node` instead of `@backstage/plugin-catalog-node/alpha`: - -- `catalogLocationsExtensionPoint` and `CatalogLocationsExtensionPoint` -- `catalogProcessingExtensionPoint` and `CatalogProcessingExtensionPoint` -- `catalogAnalysisExtensionPoint` and `CatalogAnalysisExtensionPoint` - -The old alpha exports for these extension points are now deprecated with `@deprecated` markers pointing to the new stable exports. Please update your imports from `@backstage/plugin-catalog-node/alpha` to `@backstage/plugin-catalog-node`. - -Note: The `catalogModelExtensionPoint`, `catalogPermissionExtensionPoint`, and related types remain in alpha. diff --git a/.changeset/catalog-node-use-create-service-mock.md b/.changeset/catalog-node-use-create-service-mock.md deleted file mode 100644 index 5032484ebb..0000000000 --- a/.changeset/catalog-node-use-create-service-mock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-node': patch ---- - -Updated `catalogServiceMock.mock` to use `createServiceMock` from `@backstage/backend-test-utils`, replacing the internal copy of `simpleMock`. Added `@backstage/backend-test-utils` as an optional peer dependency. diff --git a/.changeset/catalog-provider-module-failures.md b/.changeset/catalog-provider-module-failures.md deleted file mode 100644 index c7cb5a3571..0000000000 --- a/.changeset/catalog-provider-module-failures.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Failures to connect catalog providers are now attributed to the module that provided the failing provider. This means that such failures will be reported as module startup failures rather than a failure to start the catalog plugin, and will therefore respect `onPluginModuleBootFailure` configuration instead. diff --git a/.changeset/catalog-react-catalog-api-mock-shorthand.md b/.changeset/catalog-react-catalog-api-mock-shorthand.md deleted file mode 100644 index 0761dffa0e..0000000000 --- a/.changeset/catalog-react-catalog-api-mock-shorthand.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -The `catalogApiMock` test utility now returns a `MockWithApiFactory`, allowing it to be passed directly to test utilities like `renderTestApp` and `TestApiProvider` without needing the `[catalogApiRef, catalogApiMock()]` tuple. diff --git a/.changeset/catalog-remove-entity-transaction.md b/.changeset/catalog-remove-entity-transaction.md new file mode 100644 index 0000000000..e8bd5f5c8f --- /dev/null +++ b/.changeset/catalog-remove-entity-transaction.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Improved catalog entity deletion so parent invalidation and deferred relation restitch scheduling are coordinated more safely. diff --git a/.changeset/chatty-tips-stop.md b/.changeset/chatty-tips-stop.md deleted file mode 100644 index 7b4c435ffc..0000000000 --- a/.changeset/chatty-tips-stop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-auth0-provider': minor ---- - -feat: Added organization option to authorization params of the strategy diff --git a/.changeset/chatty-wasps-sink.md b/.changeset/chatty-wasps-sink.md new file mode 100644 index 0000000000..7f12c5372c --- /dev/null +++ b/.changeset/chatty-wasps-sink.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + +Affected components: Container diff --git a/.changeset/chubby-suns-film.md b/.changeset/chubby-suns-film.md deleted file mode 100644 index 440d65d707..0000000000 --- a/.changeset/chubby-suns-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fixed dark theme `--bui-fg-secondary` and `--bui-fg-disabled` tokens using black-based `oklch(0% ...)` instead of white-based `oklch(100% ...)`, making secondary and disabled text visible on dark backgrounds. diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md deleted file mode 100644 index ea4ae25aa6..0000000000 --- a/.changeset/clean-bags-occur.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Replaced `Surface` / `onSurface` system with new provider/consumer background system - -The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer `bg` architecture. - -**Types:** - -- `ContainerBg` — `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'` -- `ProviderBg` — `ContainerBg | 'neutral-auto'` - -Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. See "Neutral level capping" below for details on how levels are bounded. - -**Hooks:** - -- `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no `bg` is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. -- `useBgConsumer()` — for consumer components. Returns the parent container's `bg` unchanged. - -**Component roles:** - -- **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. **Transparent by default** — they do _not_ auto-increment; pass `bg="neutral-auto"` explicitly if you want automatic neutral stepping. -- **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit the parent container's `bg` unchanged. -- **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Card passes `bg="neutral-auto"` to its inner Box, so it auto-increments from the parent context. - -**Neutral level capping:** - -Provider components cap at `neutral-3`. There is no `neutral-4` prop value. The `neutral-4` level exists only in consumer component CSS — for example, a Button sitting on a `neutral-3` surface uses `neutral-4` tokens internally via `data-on-bg`. - -**Migration:** - -Rename the `surface` prop to `bg` on provider components and update values: - -```diff -- -+ - -- -+ - -- -+ - -- -+ -``` - -Remove `onSurface` from consumer components — they now always inherit from the parent container: - -```diff -- -+ -``` - -**Affected components:** Button diff --git a/.changeset/tired-sides-share.md b/.changeset/tired-sides-share.md deleted file mode 100644 index 661a7aee50..0000000000 --- a/.changeset/tired-sides-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-react': patch ---- - -Internal refactor to move implementation of blueprints from `@backstage/frontend-plugin-api` to this package. diff --git a/.changeset/tricky-beans-watch.md b/.changeset/tricky-beans-watch.md deleted file mode 100644 index 770efffb48..0000000000 --- a/.changeset/tricky-beans-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-events-backend-module-google-pubsub': minor ---- - -Added an optional `filter` property to PubSub consumers/publishers diff --git a/.changeset/twelve-dolls-tap.md b/.changeset/twelve-dolls-tap.md deleted file mode 100644 index 33c184b39d..0000000000 --- a/.changeset/twelve-dolls-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fixed React 17 compatibility by using `useId` from `react-aria` instead of the built-in React hook which is only available in React 18+. diff --git a/.changeset/twenty-clubs-itch.md b/.changeset/twenty-clubs-itch.md deleted file mode 100644 index c69aabec7d..0000000000 --- a/.changeset/twenty-clubs-itch.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fixed Switch component disabled state styling to show `not-allowed` cursor and disabled text color. - -**Affected components:** Switch diff --git a/.changeset/twenty-worlds-create.md b/.changeset/twenty-worlds-create.md new file mode 100644 index 0000000000..e7e4770938 --- /dev/null +++ b/.changeset/twenty-worlds-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Adds a new metrics service mock to be leveraged in tests diff --git a/.changeset/two-lies-leave.md b/.changeset/two-lies-leave.md new file mode 100644 index 0000000000..2d83758940 --- /dev/null +++ b/.changeset/two-lies-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +create scaffolder MCP action to dry run a provided scaffolder template diff --git a/.changeset/ui-searchfield-textfield-bg-focus.md b/.changeset/ui-searchfield-textfield-bg-focus.md new file mode 100644 index 0000000000..9f9cc02768 --- /dev/null +++ b/.changeset/ui-searchfield-textfield-bg-focus.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +`SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token. + +**Affected components:** SearchField, TextField diff --git a/.changeset/ui-standard-build.md b/.changeset/ui-standard-build.md deleted file mode 100644 index d0d8bd7f65..0000000000 --- a/.changeset/ui-standard-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Migrated to use the standard `backstage-cli package build` for CSS bundling instead of a custom build script. diff --git a/.changeset/use-api-holder-no-throw.md b/.changeset/use-api-holder-no-throw.md new file mode 100644 index 0000000000..465cc9f401 --- /dev/null +++ b/.changeset/use-api-holder-no-throw.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/core-plugin-api': patch +--- + +Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context. diff --git a/.changeset/vast-rockets-dig.md b/.changeset/vast-rockets-dig.md deleted file mode 100644 index 3340fabb2c..0000000000 --- a/.changeset/vast-rockets-dig.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-gitlab-provider': minor -'@backstage/plugin-catalog-backend-module-gitlab': minor ---- - -Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. diff --git a/.changeset/violet-friends-buy.md b/.changeset/violet-friends-buy.md new file mode 100644 index 0000000000..a2a620b870 --- /dev/null +++ b/.changeset/violet-friends-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. diff --git a/.changeset/warm-colts-admire.md b/.changeset/warm-colts-admire.md deleted file mode 100644 index 08fe3de363..0000000000 --- a/.changeset/warm-colts-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-node': minor ---- - -Introduced the `catalogScmEventsServiceRef`, along with `CatalogScmEventsService` and associated types. These allow communicating a unified set of events, that parts of the catalog can react to. diff --git a/.changeset/wet-cups-juggle.md b/.changeset/wet-cups-juggle.md deleted file mode 100644 index 5e1185f7df..0000000000 --- a/.changeset/wet-cups-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Removed link styles from LinkButton to avoid styling inconsistencies related to import order. diff --git a/.changeset/wicked-walls-accept.md b/.changeset/wicked-walls-accept.md deleted file mode 100644 index 6f16088ed9..0000000000 --- a/.changeset/wicked-walls-accept.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@backstage/ui': minor ---- - -**BREAKING**: Removed link and tint color tokens, added new status foreground tokens, and improved Link component styling - -The following color tokens have been removed: - -- `--bui-fg-link` (and all related tokens: `-hover`, `-pressed`, `-disabled`) -- `--bui-fg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) -- `--bui-bg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) -- `--bui-border-tint` (and all related tokens) - -**New Status Tokens:** - -Added dedicated tokens for status colors that distinguish between usage on status backgrounds vs. standalone usage: - -- `--bui-fg-danger-on-bg` / `--bui-fg-danger` -- `--bui-fg-warning-on-bg` / `--bui-fg-warning` -- `--bui-fg-success-on-bg` / `--bui-fg-success` -- `--bui-fg-info-on-bg` / `--bui-fg-info` - -The `-on-bg` variants are designed for text on colored backgrounds, while the base variants are for standalone status indicators with improved visibility and contrast. - -**Migration:** - -For link colors, migrate to one of the following alternatives: - -```diff -.custom-link { -- color: var(--bui-fg-link); -+ color: var(--bui-fg-info); /* For informational links */ -+ /* or */ -+ color: var(--bui-fg-primary); /* For standard text links */ -} -``` - -For tint colors (backgrounds, foregrounds, borders), migrate to appropriate status or neutral colors: - -```diff -.info-section { -- background: var(--bui-bg-tint); -+ background: var(--bui-bg-info); /* For informational sections */ -+ /* or */ -+ background: var(--bui-bg-neutral-1); /* For neutral emphasis */ -} -``` - -If you're using status foreground colors on colored backgrounds, update to the new `-on-bg` tokens: - -```diff -.error-badge { -- color: var(--bui-fg-danger); -+ color: var(--bui-fg-danger-on-bg); - background: var(--bui-bg-danger); -} -``` - -**Affected components:** Link diff --git a/.changeset/wide-pianos-pay.md b/.changeset/wide-pianos-pay.md new file mode 100644 index 0000000000..1cbcfd993c --- /dev/null +++ b/.changeset/wide-pianos-pay.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + +**Affected components:** Alert, Container, DialogBody, FieldLabel diff --git a/.changeset/wild-emus-write.md b/.changeset/wild-emus-write.md deleted file mode 100644 index 5c86a1d07d..0000000000 --- a/.changeset/wild-emus-write.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Migrated `EntityRelationWarning` and `EntityProcessingErrorsPanel` components from Material UI to Backstage UI. diff --git a/.changeset/wise-rabbits-double.md b/.changeset/wise-rabbits-double.md deleted file mode 100644 index 6164c6e95c..0000000000 --- a/.changeset/wise-rabbits-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/filter-predicates': minor ---- - -Introduced package, basically as the extracted predicate types from `@backstage/plugin-catalog-react/alpha` diff --git a/.changeset/yellow-ties-dream.md b/.changeset/yellow-ties-dream.md deleted file mode 100644 index 8326b28319..0000000000 --- a/.changeset/yellow-ties-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Migrate audit events reference docs to http://backstage.io/docs. diff --git a/.changeset/yellow-years-run.md b/.changeset/yellow-years-run.md deleted file mode 100644 index fc57664911..0000000000 --- a/.changeset/yellow-years-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-dynamic-feature-service': patch ---- - -Updated README for backend-dynamic-feature-service diff --git a/.changeset/young-pens-wash.md b/.changeset/young-pens-wash.md deleted file mode 100644 index 0deeb40a65..0000000000 --- a/.changeset/young-pens-wash.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@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-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 -'@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/plugin-search-backend': patch -'@backstage/core-app-api': patch -'@backstage/plugin-catalog-react': patch -'@backstage/repo-tools': patch -'@backstage/plugin-scaffolder': patch -'@backstage/cli-node': patch -'@backstage/plugin-auth-node': patch -'@backstage/cli': patch -'@backstage/plugin-home': patch -'@backstage/plugin-app': patch ---- - -Bump to latest zod to ensure it has the latest features diff --git a/.changeset/young-squids-end.md b/.changeset/young-squids-end.md new file mode 100644 index 0000000000..9ea2cee92b --- /dev/null +++ b/.changeset/young-squids-end.md @@ -0,0 +1,10 @@ +--- +'@backstage/ui': minor +--- + +Removed redundant `selected` and `indeterminate` props from the `Checkbox` component. Use the `isSelected` and `isIndeterminate` props instead, which are the standard React Aria props and already handle both the checkbox behaviour and the corresponding CSS data attributes. + +**Migration:** +Replace any usage of the `selected` and `indeterminate` props on `Checkbox` with the `isSelected` and `isIndeterminate` props. Note that the checked state and related CSS data attributes (such as `data-selected` and `data-indeterminate`) are now driven by React Aria, so any custom logic that previously inspected or set these via the old props should instead rely on the React Aria-managed state and attributes exposed through the new props. + +**Affected components:** Checkbox diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 170a4eb868..0000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -Follow the instructions at /.github/copilot-instructions.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 120000 index 0000000000..be77ac83a1 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc deleted file mode 100644 index 3a81a0cb5f..0000000000 --- a/.cursor/rules/general.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: General project guidelines for Backstage development -globs: -alwaysApply: true ---- - -Follow the instructions at /.github/copilot-instructions.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e1cece8896..e6dc0e98f5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,8 +3,8 @@ "forwardPorts": [3000, 7007], "build": { "dockerfile": "Dockerfile" }, "features": { - "ghcr.io/devcontainers/features/common-utils:2.5.6": {}, - "ghcr.io/devcontainers-contrib/features/mkdocs:2": {} + "ghcr.io/devcontainers/features/common-utils:2.5.7": {}, + "ghcr.io/devcontainers-extra/features/mkdocs:2": {} }, "postCreateCommand": "bash .devcontainer/setup.sh", "hostRequirements": { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 725c6c676b..7e5cc36bdf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,8 +12,10 @@ yarn.lock @backstage/maintainers @backst /.github @backstage/operations-maintainers /.github/vale @backstage/maintainers @backstage/documentation-maintainers /.patches @backstage/operations-maintainers +/.storybook @backstage/design-system-maintainers /beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers /docs @backstage/maintainers @backstage/documentation-maintainers +/docs-ui @backstage/design-system-maintainers /docs/assets/search @backstage/search-maintainers /docs/auth @backstage/auth-maintainers /docs/backend-system @backstage/framework-maintainers diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2157025b7b..9ded15067a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,49 +1,3 @@ -Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces. - -## Key Directories - -- `/packages`: Core framework packages (prefixed `@backstage/`) -- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`) -- `/packages/app` and `/packages/backend`: Example app for local development -- `/packages/app`: Main example app using the new frontend system -- `/packages/app-legacy`: Example app using the old frontend system -- `/docs`: Documentation files - -Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system. - -## Code Standards - -The following files contain guidelines for the project: - -- `/CONTRIBUTING.md`: comprehensive contribution guidelines. -- `/STYLE.md`: guidelines for code style. -- `/REVIEWING.md`: guidelines for pull requests and writing changesets. -- `/SECURITY.md`: guidelines for security. -- `/docs/architecture-decisions/`: contains the architecture decisions for the project. - -## Development Flow - -Before any of these commands can be run, you need to run `yarn install` in the project root. - -- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. -- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. -- Type checking: Use `yarn tsc` in the project root to run the type checker. -- Code formatting: Use `yarn prettier --write ` to format code. -- Lint: Use `yarn lint --fix` in the project root to run the linter. -- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages. -- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007). -- Create: Use `yarn new` to scaffold new plugins, packages, or modules. - -You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows. - -All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. - -Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested. - -## Repository Structure - -See `/docs/contribute/project-structure.md` for a detailed description of the repository structure. - ## Pull Request Reviews When reviewing a pull request you MUST calculate a priority score between 0 and 100, where a higher score indicates higher priority. You MUST include the score as an HTML comment at the very beginning of the "PR Overview" section, using the following format: ``. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..1fb8ca3ca3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + ignore: + # These need to stay pinned to match the React/Node versions we support + - dependency-name: '@types/react' + - dependency-name: '@types/node' + # Ignored due to licensing issues. See #10992 + - dependency-name: '@elastic/elasticsearch' + - dependency-name: 'event-source-polyfill' + # Disable version updates since we use Renovate for that + open-pull-requests-limit: 0 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index da3cf3e8fb..4e5ca932c0 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -18,6 +18,9 @@ // ignored due to licensing issues. See #10992 '@elastic/elasticsearch', 'event-source-polyfill', + // These need to stay pinned to match the React/Node versions we support + '@types/react', + '@types/node', ], packageRules: [ { diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 779cc00cf7..f3bb31015b 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -30,6 +30,7 @@ backend's backported backporting BEPs +bg bigint Bigtable Billett @@ -245,6 +246,7 @@ Levenshtein lightbox Lightsail limitranges +liveness LocalStack lockdown lockfile @@ -322,6 +324,7 @@ openapi OpenSearch OpenShift openssl +opentelemetry orgs overridable padding @@ -422,7 +425,9 @@ SCM SCMs scrollable scrollbar +scrollbars sdks +SearchField seb semlas semver @@ -492,6 +497,7 @@ templater Templater templaters Templaters +TextField TFRecord theia Themer @@ -525,6 +531,7 @@ unassign unbreak Unconference undici +ungrouped unicode unmanaged unmount @@ -548,6 +555,7 @@ validators Valkey varchar viewport +virtualized vite VMware Vodafone @@ -574,3 +582,4 @@ zsh resizable enums LLMs +cleye diff --git a/.github/workflows/api-breaking-changes-comment.yml b/.github/workflows/api-breaking-changes-comment.yml index aca727743a..82e3310ec1 100644 --- a/.github/workflows/api-breaking-changes-comment.yml +++ b/.github/workflows/api-breaking-changes-comment.yml @@ -23,7 +23,7 @@ jobs: comment-cache-key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index 1c9e001c5e..2c53aec0de 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -27,13 +27,13 @@ jobs: run: git fetch --depth 1 origin ${{ github.base_ref }} - name: setup-node - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: linux-v22 diff --git a/.github/workflows/automate_area-labels.yml b/.github/workflows/automate_area-labels.yml index cdf4a28b55..90b8ea9d16 100644 --- a/.github/workflows/automate_area-labels.yml +++ b/.github/workflows/automate_area-labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 343eb9725b..34b1791ca8 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -34,7 +34,7 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + - uses: backstage/actions/changeset-feedback@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 name: Generate feedback with: diff-ref: 'origin/master' diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml index c1ecde9194..62db24ce17 100644 --- a/.github/workflows/automate_merge_message.yml +++ b/.github/workflows/automate_merge_message.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/automate_stale.yml b/.github/workflows/automate_stale.yml index c9ee8f8078..4bed28af3a 100644 --- a/.github/workflows/automate_stale.yml +++ b/.github/workflows/automate_stale.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - name: Stale check - base - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: stale-issue-message: > This issue has been automatically marked as stale because it has not had @@ -42,7 +42,7 @@ jobs: operations-per-run: 100 - name: Stale check - bugs without repro - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: stale-issue-message: > This bug report has been automatically marked as stale because it has not had diff --git a/.github/workflows/ci-noop.yml b/.github/workflows/ci-noop.yml index d9851778c6..71b0c68791 100644 --- a/.github/workflows/ci-noop.yml +++ b/.github/workflows/ci-noop.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - 'microsite/**' + - '!microsite/data/**' - 'beps/**' permissions: @@ -40,7 +41,7 @@ jobs: name: Test ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aeddfcea1d..bb5474506d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,11 @@ name: CI on: # NOTE: If you change these you must update ci-noop.yml as well pull_request: - paths-ignore: - - 'microsite/**' - - 'beps/**' + paths: + - '**' + - '!microsite/**' + - 'microsite/data/**' + - '!beps/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -28,7 +30,7 @@ jobs: name: Install ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -47,13 +49,13 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -76,20 +78,20 @@ jobs: name: Verify ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -239,13 +241,13 @@ jobs: run: git fetch origin ${{ github.event.pull_request.base.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index d12d844c66..61f8a2ce51 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -10,11 +10,11 @@ jobs: timeout-minutes: 10 steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - - uses: backstage/actions/cron@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + - uses: backstage/actions/cron@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 2accec861a..18974a3520 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -31,13 +31,13 @@ jobs: ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -62,7 +62,7 @@ jobs: uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build and push - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: './example-app' file: ./example-app/packages/backend/Dockerfile diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index bd963252b4..bc45c28cc1 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -23,49 +23,50 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - - name: find latest release + - name: find latest release branch uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: find-release with: script: | - const { data } = await github.rest.repos.listTags({ + const data = await octokit.paginate(github.rest.repos.listBranches, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100, }) - const [{tag}] = data + const [{branch}] = data .map(i => i.name) - .filter(tag => tag.match(/^v\d+\.\d+\.\d+$/)) - .map(tag => ({ - tag, - val: tag + .filter(branch => branch.match(/^patch\/v\d+\.\d+\.\d+$/)) + .map(branch => ({ + branch, + val: branch + .split('/')[1] .slice(1) .split('.') .reduce((val, part) => Number(val) * 1000 + Number(part)) })) .sort((a, b) => b.val - a.val) - return tag + return branch result-encoding: string - name: checkout latest release uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: refs/tags/${{ steps.find-release.outputs.result }} + ref: refs/heads/${{ steps.find-release.outputs.result }} - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -124,7 +125,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -132,13 +133,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -218,12 +219,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth @@ -232,7 +233,7 @@ jobs: - name: checkout latest release uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: refs/tags/${{ needs.stable.outputs.release }} + ref: refs/heads/${{ needs.stable.outputs.release }} - name: microsite yarn install run: yarn install --immutable diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 8dc1a71888..37e188a1f0 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -71,12 +71,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -147,7 +147,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index 791ca69a87..4ba57ebb00 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/mui-migration-tracker.yml b/.github/workflows/mui-migration-tracker.yml index ac5af3f47b..8cd3023cc3 100644 --- a/.github/workflows/mui-migration-tracker.yml +++ b/.github/workflows/mui-migration-tracker.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -26,13 +26,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c1c55f7abc..aa04752c29 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 + uses: github/codeql-action/upload-sarif@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_bui-docs.yml b/.github/workflows/sync_bui-docs.yml index 6acdc48cc7..58491479cf 100644 --- a/.github/workflows/sync_bui-docs.yml +++ b/.github/workflows/sync_bui-docs.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -16,13 +16,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index fb82722b71..38a119edbc 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -20,12 +20,12 @@ jobs: fetch-depth: 0 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index fb51949fd1..2b1dd1098f 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -11,21 +11,80 @@ jobs: if: github.actor == 'dependabot[bot]' && github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit + - name: Approve yarn.lock-only PRs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + if (files.some(file => !file.filename.endsWith("yarn.lock"))) { + console.log("Skipping approval since some files are not yarn.lock"); + return; + } + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + event: "APPROVE", + }); + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 ref: ${{ github.head_ref }} token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + - name: Verify Dependabot branch + id: verify + run: | + if git branch --show-current | grep -q '^dependabot/'; then + echo "is_dependabot=true" >> "$GITHUB_OUTPUT" + else + echo "Not a dependabot branch, skipping" + echo "is_dependabot=false" >> "$GITHUB_OUTPUT" + fi - name: Configure Git + if: steps.verify.outputs.is_dependabot == 'true' run: | git config --global user.email noreply@backstage.io git config --global user.name 'Github changeset workflow' + - name: Setup Node + if: steps.verify.outputs.is_dependabot == 'true' + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 22.x + - name: Dedupe lockfiles + if: steps.verify.outputs.is_dependabot == 'true' + id: dedupe + run: | + corepack enable + changed=false + + for dir in . docs-ui microsite; do + if [ "$dir" = "." ]; then lockfile=yarn.lock; else lockfile="$dir/yarn.lock"; fi + if git diff --name-only HEAD~1 | grep -q "^${lockfile}$"; then + yarn --cwd "$dir" dedupe + if ! git diff --quiet "$lockfile"; then + git add "$lockfile" + changed=true + fi + fi + done + + echo "changed=$changed" >> "$GITHUB_OUTPUT" - name: Generate changeset + if: steps.verify.outputs.is_dependabot == 'true' + id: changeset uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | @@ -50,12 +109,6 @@ jobs: await fs.writeFile(fileName, body); } - const branch = await exec.getExecOutput('git branch --show-current'); - if (!branch.stdout.startsWith('dependabot/')) { - console.log('Not a dependabot branch, skipping'); - return; - } - const diffOutput = await exec.getExecOutput('git diff --name-only HEAD~1'); const diffFiles = diffOutput.stdout.split('\n'); @@ -79,5 +132,9 @@ jobs: const { stdout: commitMessage } = await exec.getExecOutput('git show --pretty=format:%s -s HEAD'); await createChangeset(fileName, commitMessage, packageNames); await exec.exec('git', ['add', fileName]); - await exec.exec('git commit -C HEAD --amend --no-edit'); - await exec.exec('git push --force'); + core.setOutput('changed', 'true'); + - name: Commit and push + if: steps.verify.outputs.is_dependabot == 'true' && (steps.dedupe.outputs.changed == 'true' || steps.changeset.outputs.changed == 'true') + run: | + git commit -C HEAD --amend --no-edit --no-verify + git push --force diff --git a/.github/workflows/sync_patch-release.yml b/.github/workflows/sync_patch-release.yml index e126fe2ab9..628635af1d 100644 --- a/.github/workflows/sync_patch-release.yml +++ b/.github/workflows/sync_patch-release.yml @@ -20,7 +20,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -32,7 +32,7 @@ jobs: token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/sync_pull-requests-scheduled.yml b/.github/workflows/sync_pull-requests-scheduled.yml new file mode 100644 index 0000000000..3854a0f4df --- /dev/null +++ b/.github/workflows/sync_pull-requests-scheduled.yml @@ -0,0 +1,94 @@ +name: Sync Pull Requests Scheduled + +on: + schedule: + # Every 30 minutes, processing a rotating batch of PRs + - cron: '*/30 * * * *' + workflow_dispatch: + +permissions: {} + +jobs: + plan: + permissions: + pull-requests: read + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + matrix: ${{ steps.batch.outputs.matrix }} + count: ${{ steps.batch.outputs.count }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Determine PR batch + id: batch + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + // 48 slots per day (one per 30-minute cron window). + // Each PR is assigned a fixed slot via pr.number % 48, so + // opening/closing other PRs never shifts the assignment. + const totalSlots = 48; + const now = new Date(); + const currentSlot = + now.getUTCHours() * 2 + (now.getUTCMinutes() >= 30 ? 1 : 0); + + const prs = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + const batch = prs.filter( + pr => pr.number % totalSlots === currentSlot, + ); + + core.info( + `Slot ${currentSlot}/${totalSlots}: ` + + `${batch.length} PRs to process out of ${prs.length} open`, + ); + + const matrix = { + include: batch.map(pr => ({ prNumber: String(pr.number) })), + }; + + core.setOutput('matrix', JSON.stringify(matrix)); + core.setOutput('count', String(batch.length)); + + sync: + needs: plan + if: needs.plan.outputs.count > 0 + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} + fail-fast: false + max-parallel: 1 + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Backstage PR automation + uses: backstage/actions/pr-automation@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 + with: + app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} + private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} + installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} + project-owner: backstage + project-number: '14' + pr-number: ${{ matrix.prNumber }} + action: synchronize + required-checks: | + DCO + E2E Linux 22.x + E2E Linux 24.x + Test 22.x + Test 24.x + Verify 22.x + Verify 24.x diff --git a/.github/workflows/sync_pull-requests-trigger.yml b/.github/workflows/sync_pull-requests-trigger.yml index 152837b811..03d29c262d 100644 --- a/.github/workflows/sync_pull-requests-trigger.yml +++ b/.github/workflows/sync_pull-requests-trigger.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/sync_pull-requests.yml b/.github/workflows/sync_pull-requests.yml index 7868415af5..aa7088bf23 100644 --- a/.github/workflows/sync_pull-requests.yml +++ b/.github/workflows/sync_pull-requests.yml @@ -15,7 +15,7 @@ jobs: github.event.workflow_run.event == 'pull_request' steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -48,7 +48,7 @@ jobs: - name: Backstage PR automation if: steps.context.outputs.pr-number - uses: backstage/actions/pr-automation@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/pr-automation@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 86ec13f487..62478a7891 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -19,13 +19,13 @@ jobs: ref: v${{ github.event.client_payload.version }} - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -81,9 +81,5 @@ jobs: owner: 'backstage', repo: 'upgrade-helper-diff', workflow_id: 'release.yml', - ref: 'master', - inputs: { - version: require('./packages/create-app/package.json').version, - releaseVersion: require('./package.json').version - }, + ref: 'master' }); diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 8575cab760..04495e606b 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -11,10 +11,33 @@ jobs: if: github.actor == 'renovate[bot]' && github.repository == 'backstage/backstage' steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit + - name: Approve yarn.lock-only PRs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + script: | + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + if (files.some(file => !file.filename.endsWith("yarn.lock"))) { + console.log("Skipping approval since some files are not yarn.lock"); + return; + } + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + event: "APPROVE", + }); + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 34d73e1043..b04c5b9ab7 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -12,19 +12,19 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index a8b364fbb8..8bb4947dab 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 + uses: github/codeql-action/upload-sarif@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 with: sarif_file: snyk.sarif diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 7df18100b0..022ad073c4 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -30,7 +30,7 @@ jobs: token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_accessibility-noop.yml b/.github/workflows/verify_accessibility-noop.yml index 51caba5acd..8cf164ecfa 100644 --- a/.github/workflows/verify_accessibility-noop.yml +++ b/.github/workflows/verify_accessibility-noop.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index 033d325313..83d27612ae 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -20,17 +20,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x - name: run Lighthouse CI diff --git a/.github/workflows/verify_chromatic-noop.yml b/.github/workflows/verify_chromatic-noop.yml index cb4c96902a..5fe97c8903 100644 --- a/.github/workflows/verify_chromatic-noop.yml +++ b/.github/workflows/verify_chromatic-noop.yml @@ -20,7 +20,7 @@ jobs: name: Chromatic steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml index c84aa692ca..26eaeb5f45 100644 --- a/.github/workflows/verify_chromatic.yml +++ b/.github/workflows/verify_chromatic.yml @@ -24,7 +24,7 @@ jobs: name: Chromatic steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -34,13 +34,13 @@ jobs: fetch-depth: 10000 # Required to retrieve git history - name: Use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: Install dependencies - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index a919dff7b0..bb6b659db2 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 + uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 + uses: github/codeql-action/autobuild@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3 + uses: github/codeql-action/analyze@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index af42f25390..792402f7ad 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -7,29 +7,48 @@ on: - '**.md' jobs: - check-all-files: + check-docs: + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - # Vale does not support file excludes, so we use the script to generate a list of files instead - # The action also does not allow args or a local config file to be passed in, so the files array - # also contains an "--config=.github/vale/config.ini" option - - name: generate vale args - id: generate - run: echo "args=$(node scripts/check-docs-quality.js --ci-args)" >> $GITHUB_OUTPUT + - name: Install vale + run: | + VALE_VERSION="3.7.1" + VALE_CHECKSUM="ba4924bf2c5884499f09b02a6eb3318b29df40a3e81701c0804b9b1aefcd9483" + VALE_DIST_DIR="/tmp/vale-dist" + VALE_ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" - - name: documentation quality check - uses: errata-ai/vale-action@d89dee975228ae261d22c15adcd03578634d429c # v2.1.1 - with: - # This also contains --config=.github/vale/config.ini ... :/ - files: '${{ steps.generate.outputs.args }}' - version: latest + mkdir -p "$VALE_DIST_DIR" + + # Download pinned Vale binary + curl -fsSL -H "Authorization: token $GH_TOKEN" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${VALE_ARCHIVE}" -o "$VALE_DIST_DIR/$VALE_ARCHIVE" + + # Verify the integrity of the downloaded archive + echo "$VALE_CHECKSUM $VALE_DIST_DIR/$VALE_ARCHIVE" | sha256sum --check + + mkdir -p "$HOME/.local/bin" + tar -xzf "$VALE_DIST_DIR/$VALE_ARCHIVE" -C "$HOME/.local/bin" vale + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + vale --version env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + + - name: Get changed files + run: | + gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --paginate -q '.[] | select(.status != "removed") | .filename' > /tmp/pr-files.txt + env: + GH_TOKEN: ${{ github.token }} + + - name: Documentation quality check + run: node scripts/check-docs-quality.js --ci /tmp/pr-files.txt diff --git a/.github/workflows/verify_e2e-linux-noop.yml b/.github/workflows/verify_e2e-linux-noop.yml index 38ee55f72c..dbba242bae 100644 --- a/.github/workflows/verify_e2e-linux-noop.yml +++ b/.github/workflows/verify_e2e-linux-noop.yml @@ -29,7 +29,7 @@ jobs: name: E2E Linux ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 5a5d637a9e..66a176e128 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -43,7 +43,7 @@ jobs: name: E2E Linux ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -67,12 +67,12 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 45b713aab5..370d10310d 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -32,7 +32,7 @@ jobs: name: Techdocs steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -42,11 +42,11 @@ jobs: python-version: '3.9' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-windows-noop.yml b/.github/workflows/verify_e2e-windows-noop.yml index 4759555905..baa18cb53b 100644 --- a/.github/workflows/verify_e2e-windows-noop.yml +++ b/.github/workflows/verify_e2e-windows-noop.yml @@ -25,7 +25,7 @@ jobs: name: E2E Windows ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index c9e2246d64..f4fded1a0d 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -33,7 +33,7 @@ jobs: name: E2E Windows ${{ matrix.node-version }} steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -52,7 +52,7 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -80,7 +80,7 @@ jobs: uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # latest - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml index 84d666f00e..80cd7907e2 100644 --- a/.github/workflows/verify_fossa.yml +++ b/.github/workflows/verify_fossa.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_microsite-noop.yml b/.github/workflows/verify_microsite-noop.yml index acab96962c..08eeac4431 100644 --- a/.github/workflows/verify_microsite-noop.yml +++ b/.github/workflows/verify_microsite-noop.yml @@ -21,7 +21,7 @@ jobs: name: Microsite steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index d4441c3ea9..b8ce5bfb93 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -28,49 +28,50 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - - name: find latest release + - name: find latest release branch uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: find-release with: script: | - const { data } = await github.rest.repos.listTags({ + const data = await octokit.paginate(github.rest.repos.listBranches, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100, }) - const [{tag}] = data + const [{branch}] = data .map(i => i.name) - .filter(tag => tag.match(/^v\d+\.\d+\.\d+$/)) - .map(tag => ({ - tag, - val: tag + .filter(branch => branch.match(/^patch\/v\d+\.\d+\.\d+$/)) + .map(branch => ({ + branch, + val: branch + .split('/')[1] .slice(1) .split('.') .reduce((val, part) => Number(val) * 1000 + Number(part)) })) .sort((a, b) => b.val - a.val) - return tag + return branch result-encoding: string - name: checkout latest release uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: refs/tags/${{ steps.find-release.outputs.result }} + ref: refs/heads/${{ steps.find-release.outputs.result }} - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -126,7 +127,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit @@ -134,13 +135,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c3038aa5576b9cd845ccc518ef854d3660e8cb40 # v0.7.6 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -212,14 +213,14 @@ jobs: name: Microsite steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 diff --git a/.github/workflows/verify_microsite_accessibility-noop.yml b/.github/workflows/verify_microsite_accessibility-noop.yml index 87963dd78e..a90b892fae 100644 --- a/.github/workflows/verify_microsite_accessibility-noop.yml +++ b/.github/workflows/verify_microsite_accessibility-noop.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index 76862a9fa7..226f1f71be 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js 22.x - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22.x diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 7651160795..3e74e088ec 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -29,14 +29,14 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.patches/pr-32409.txt b/.patches/pr-32409.txt deleted file mode 100644 index a14d9feb37..0000000000 --- a/.patches/pr-32409.txt +++ /dev/null @@ -1 +0,0 @@ -Fixes a deprecation warning for Router extension in tests \ No newline at end of file diff --git a/.patches/pr-32428.txt b/.patches/pr-32428.txt deleted file mode 100644 index 77136cf4c8..0000000000 --- a/.patches/pr-32428.txt +++ /dev/null @@ -1 +0,0 @@ -fix(ui): restore React 17 compatibility for useId \ No newline at end of file diff --git a/.patches/pr-32524.txt b/.patches/pr-32524.txt deleted file mode 100644 index d5aa035f3f..0000000000 --- a/.patches/pr-32524.txt +++ /dev/null @@ -1 +0,0 @@ -Depend on a version of the zod library that has the version 3 and 4 exports \ No newline at end of file diff --git a/.patches/pr-32973.txt b/.patches/pr-32973.txt new file mode 100644 index 0000000000..6d83c5d66d --- /dev/null +++ b/.patches/pr-32973.txt @@ -0,0 +1 @@ +Fixes the search component to register the first search that happens on initial navigation to the search page. \ No newline at end of file diff --git a/.patches/pr-32996.txt b/.patches/pr-32996.txt new file mode 100644 index 0000000000..2dd0fde602 --- /dev/null +++ b/.patches/pr-32996.txt @@ -0,0 +1 @@ +Fixes the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. \ No newline at end of file diff --git a/.patches/pr-33004.txt b/.patches/pr-33004.txt new file mode 100644 index 0000000000..5594a76e6b --- /dev/null +++ b/.patches/pr-33004.txt @@ -0,0 +1 @@ +Fixes entity page tab groups not respecting the ordering from the groups configuration. \ No newline at end of file diff --git a/.patches/pr-33034.txt b/.patches/pr-33034.txt new file mode 100644 index 0000000000..4648da00b2 --- /dev/null +++ b/.patches/pr-33034.txt @@ -0,0 +1 @@ +Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports to `@backstage/plugin-scaffolder-react`. This change was incorrectly applied to `@backstage/plugin-scaffolder` in the 1.48.2 release, which is now gone. Note that the API signature of this API has still changed since the 1.47 release. \ No newline at end of file diff --git a/.patches/pr-33057.txt b/.patches/pr-33057.txt new file mode 100644 index 0000000000..3499e84a89 --- /dev/null +++ b/.patches/pr-33057.txt @@ -0,0 +1 @@ +Fixed a type inference issue with the `apis` option of testing utilities in `@backstage/frontend-test-utils`. \ No newline at end of file diff --git a/.storybook/modes.ts b/.storybook/modes.ts index 7087fa35eb..ae4ff76a7c 100644 --- a/.storybook/modes.ts +++ b/.storybook/modes.ts @@ -15,4 +15,19 @@ export const allModes = { themeMode: 'dark', themeName: 'spotify', }, + 'light spotify neutral-1': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-1', + }, + 'light spotify neutral-2': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-2', + }, + 'light spotify neutral-3': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-3', + }, } as const; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6a0177cafe..022b6123e2 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,7 +3,7 @@ import addonDocs from '@storybook/addon-docs'; import addonThemes from '@storybook/addon-themes'; import addonLinks from '@storybook/addon-links'; import { definePreview } from '@storybook/react-vite'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { TestApiProvider } from '@backstage/test-utils'; import { AlertDisplay } from '@backstage/core-components'; import { apis } from './support/apis'; @@ -19,6 +19,7 @@ import './storybook.css'; // Custom themes import './themes/spotify.css'; +import { Box } from '../packages/ui/src/components/Box'; export default definePreview({ tags: ['manifest'], @@ -30,8 +31,8 @@ export default definePreview({ toolbar: { icon: 'circlehollow', items: [ - { value: 'light', icon: 'circlehollow', title: 'Light' }, - { value: 'dark', icon: 'circle', title: 'Dark' }, + { value: 'light', icon: 'sun', title: 'Light' }, + { value: 'dark', icon: 'moon', title: 'Dark' }, ], dynamicTitle: true, }, @@ -49,11 +50,26 @@ export default definePreview({ dynamicTitle: true, }, }, + background: { + name: 'Background', + description: 'Global background for components', + defaultValue: 'app', + toolbar: { + icon: 'contrast', + items: [ + { value: 'app', title: 'App Background' }, + { value: 'neutral-1', title: 'Neutral 1 Background' }, + { value: 'neutral-2', title: 'Neutral 2 Background' }, + { value: 'neutral-3', title: 'Neutral 3 Background' }, + ], + }, + }, }, initialGlobals: { themeMode: 'light', themeName: 'backstage', + background: 'app', }, parameters: { @@ -70,7 +86,14 @@ export default definePreview({ options: { storySort: { - order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'], + order: [ + 'Backstage UI', + 'Recipes', + 'Guidelines', + 'Plugins', + 'Layout', + 'Navigation', + ], }, }, @@ -114,12 +137,14 @@ export default definePreview({ }, decorators: [ - Story => { + (Story, context) => { const [globals] = useGlobals(); const selectedTheme = globals.themeMode === 'light' ? themes.light : themes.dark; const selectedThemeMode = globals.themeMode || 'light'; const selectedThemeName = globals.themeName || 'backstage'; + const selectedBackground = globals.background || 'app'; + const isFullscreen = context.parameters.layout === 'fullscreen'; useEffect(() => { document.body.removeAttribute('data-theme-mode'); @@ -133,6 +158,8 @@ export default definePreview({ }, [selectedTheme, selectedThemeName]); document.body.style.backgroundColor = 'var(--bui-bg-app)'; + document.body.style.padding = + isFullscreen && selectedBackground !== 'app' ? '1rem' : ''; const docsStoryElements = document.getElementsByClassName('docs-story'); Array.from(docsStoryElements).forEach(element => { (element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)'; @@ -143,7 +170,19 @@ export default definePreview({ {/* @ts-ignore */} - + {Array.from({ + length: + selectedBackground === 'app' + ? 0 + : parseInt(selectedBackground.split('-')[1], 10), + }).reduce( + children => ( + + {children} + + ), + , + )} ); diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index b72683f53e..3cad0fba10 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -183,27 +183,19 @@ font-weight: var(--bui-font-weight-regular); } - .bui-HeaderToolbar { - padding-top: var(--bui-space-2); - padding-inline: var(--bui-space-2); - } - - .bui-HeaderToolbarWrapper { - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-3); + .bui-PluginHeaderToolbarWrapper { + padding: 0; + height: 32px; border: none; + background: none; } - .bui-HeaderToolbarControls { - right: calc(var(--bui-space-3) - 1px); - } - - .bui-HeaderTabsWrapper { - margin-top: var(--bui-space-2); - margin-inline: var(--bui-space-2); - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-1); + .bui-PluginHeaderTabsWrapper { + margin: 0; + padding: 0; + margin-left: -8px; border: none; + background: none; } .bui-Input { @@ -213,6 +205,10 @@ .bui-Tag { border-radius: var(--bui-radius-full); } + + .bui-Container { + padding-inline: 0; + } } [data-theme-mode='light'][data-theme-name='spotify'] { diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..010ebca9e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces. + +## Key Directories + +- `/packages`: Core framework packages (prefixed `@backstage/`) +- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`) +- `/packages/app`: Main example app using the new frontend system +- `/packages/app-legacy`: Example app using the old frontend system +- `/packages/backend`: Example backend for local development +- `/docs`: Documentation files + +Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system. + +## Code Standards + +The following files contain guidelines for the project: + +- `/CONTRIBUTING.md`: comprehensive contribution guidelines. +- `/STYLE.md`: guidelines for code style. +- `/REVIEWING.md`: guidelines for pull requests and writing changesets. +- `/SECURITY.md`: guidelines for security. +- `/docs/architecture-decisions/`: contains the architecture decisions for the project. + +When writing or generating code, always match the existing coding style of each individual package and file. Different packages in the monorepo may have different conventions — consistency within a package is more important than consistency across the repo. + +When writing or generating tests, prefer fewer thorough tests with multiple assertions over many small tests. When using React Testing Library, prefer using `screen` and `.findBy*` queries over `waitFor`, and avoid adding test IDs to the implementation. + +## Development Flow + +Before any of these commands can be run, you need to run `yarn install` in the project root. + +- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. +- Test: Use `CI=1 yarn test ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. +- Type checking: Use `yarn tsc` in the project root to run the type checker. Do not try to run it somewhere else than the project root and do not supply any options. +- Code formatting: Use `yarn prettier --write <...paths>` to format code. Run it explicitly for file paths that you know are changed, not for entire folders - otherwise it may change formatting of unrelated files. +- Lint: Use `yarn lint --fix` in the project root to run the linter. +- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages. +- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007). +- Create: Use `yarn new` to scaffold new plugins, packages, or modules. + +You MUST NOT run builds or create a release by running `yarn build`, `yarn changesets version`, or `yarn release` as part of any changes. Builds and releases are made by separate workflows. + +All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory and should be created by writing changeset files directly — never use the changeset CLI. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. For non-breaking changes that introduce new APIs or features, use `minor` for packages at version `1.0.0` or higher, and `patch` for packages below `1.0.0`. Each changeset message should be relevant to the specific package it targets and written for Backstage adopters as the audience — avoid referencing internal implementation details. If a change spans multiple packages you often need to create separate changesets to make sure they are tailored to each package. + +When creating pull requests, use the template at `/.github/PULL_REQUEST_TEMPLATE.md`. + +Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested. + +Never make changes to the release notes in `/docs/releases` unless explicitly asked. These document past releases and should not be updated based on newer changes. + +## Repository Structure + +See `/docs/contribute/project-structure.md` for a detailed description of the repository structure. diff --git a/OWNERS.md b/OWNERS.md index e09a0a24c3..4d535fc4b2 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -283,6 +283,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | | Deepankumar Loganathan | | [deepan10](https://github.com/deepan10) | `deepan10` | +| Elaine Mattos | DB Systel | [elaine-mattos](https://github.com/elaine-mattos) | `elaine_mattos` | | Gabriel Dugny | Believe | [GabDug](https://github.com/GabDug) | `GabDug` | | Heikki Hellgren | OP Financial Group | [drodil](https://github.com/drodil) | `deathammer` | | Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` | diff --git a/app-config.yaml b/app-config.yaml index ac66d223b4..5eea93084e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -27,6 +27,130 @@ app: packageName: example-app + routes: + bindings: + catalog.viewTechDoc: techdocs.docRoot + org.catalogIndex: catalog.catalogIndex + + pluginOverrides: + - match: + pluginId: pages + info: + description: 'This description was overridden in app-config.yaml' + - match: + pluginId: /^catalog(-.*)?$/ + info: + ownerEntityRefs: [cubic-belugas] + - match: + packageName: '@backstage/plugin-scaffolder' + info: + ownerEntityRefs: [cubic-belugas] + + extensions: + # set availableLanguages example + - api:app/app-language: + config: + availableLanguages: ['en', 'es', 'fr', 'de', 'ja'] + defaultLanguage: 'en' + - entity-card:org/members-list: + config: + showAggregateMembersToggle: true + initialRelationAggregation: aggregated + - entity-card:org/ownership: + config: + ownedKinds: ['Component', 'API', 'System'] + + # - apis.plugin.graphiql.browse.gitlab: true + # - graphiql-endpoint:graphiql/gitlab: true + + - nav-item:search: false + - nav-item:user-settings: false + - nav-item:catalog + - nav-item:api-docs + - nav-item:scaffolder + - nav-item:app-visualizer + + # Pages + - page:catalog/entity: + config: + showNavItemIcons: true + # default content order for all groups, can be 'title' or 'natural' + # defaultContentOrder: title + groups: + # placing a tab at the beginning + - overview: + title: Overview + # example disabling a default group + # - development: false + # example overriding a default group title + - documentation: + title: Docs + icon: docs + # example aliasing a group + # aliases: + # - docs + - deployment: + title: Deployments + # example adding a new group + - custom: + title: Custom + + # Entity page cards + - entity-card:catalog/about: + config: + type: info + - entity-card:catalog/labels + - entity-card:catalog/links: + config: + filter: + kind: component + metadata.links: + $exists: true + # filter: kind:component has:links + type: info + # - entity-card:linguist/languages + - entity-card:catalog-graph/relations: + config: + height: 300 + - entity-card:api-docs/has-apis + - entity-card:api-docs/consumed-apis + - entity-card:api-docs/provided-apis + - entity-card:api-docs/providing-components + - entity-card:api-docs/consuming-components + # Org Plugin + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile: + config: + maxRelations: 5 + hideIcons: true + # - entity-card:azure-devops/readme + + # Entity page contents + - entity-content:catalog/overview + - entity-content:api-docs/definition + - entity-content:api-docs/apis: + config: + # example overriding the default group + group: documentation + icon: kind:api + - entity-content:techdocs: + config: + icon: techdocs + - entity-content:kubernetes/kubernetes: + config: + # example disassociating from the default group + group: false + # - entity-content:azure-devops/pipelines + # - entity-content:azure-devops/pull-requests + # - entity-content:azure-devops/git-tags + + # Enable the catalog-unprocessed-entities tab in devtools + - devtools-content:catalog-unprocessed-entities: true + # Disable the catalog-unprocessed-entities element outside devtools + - page:catalog-unprocessed-entities: false + backend: # Used for enabling authentication, secret is shared by all backend plugins # See https://backstage.io/docs/auth/service-to-service-auth for @@ -68,6 +192,7 @@ backend: actions: pluginSources: - catalog + - scaffolder # See README.md in the proxy-backend plugin for information on the configuration format proxy: endpoints: @@ -218,6 +343,13 @@ auth: enabled: true allowedRedirectUriPatterns: - cursor://* + - http://localhost:* + - http://127.0.0.1:* + experimentalClientIdMetadataDocuments: + enabled: true + allowedRedirectUriPatterns: + - http://127.0.0.1:* + - http://localhost:* ### Add auth.keyStore.provider to more granularly control how to store JWK data when running # the auth-backend. diff --git a/beps/0012-metrics-service/README.md b/beps/0012-metrics-service/README.md index 59096d11d8..87b2059f02 100644 --- a/beps/0012-metrics-service/README.md +++ b/beps/0012-metrics-service/README.md @@ -23,7 +23,6 @@ creation-date: 2025-06-23 - [Integration with OpenTelemetry Auto-Instrumentation](#integration-with-opentelemetry-auto-instrumentation) - [Configuration](#configuration) - [Interface](#interface) - - [Root Metrics Service](#root-metrics-service) - [Plugin Metrics Service](#plugin-metrics-service) - [Example](#example) - [Release Plan](#release-plan) @@ -36,7 +35,7 @@ Add a core `MetricsService` to Backstage's framework to provide a unified interf ## Motivation -While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem. For example, both `catalog_entities_count` and `catalog.processed.entities.count` are examples of existing metric patterns. Ideally, these would be standardized to `backstage.plugin.catalog.entities.count` and `backstage.plugin.catalog.entities.processed.total` respectively. +While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem and incompatibility with OpenTelemetry semantic conventions. For example, a plugin implementing MCP functionality might incorrectly namespace metrics as `backstage_mcp_client_duration` when OpenTelemetry semantic conventions explicitly define `mcp.client.operation.duration` as the standard. By providing a core metrics service: @@ -45,7 +44,7 @@ By providing a core metrics service: ### Goals -- Plugin-scoped metric namespacing +- Plugin identification via OpenTelemetry Instrumentation Scope - Consistent metrics patterns across all plugins - Aligned with OpenTelemetry industry standards - Provide a familiar interface as other core services @@ -124,9 +123,12 @@ The `MetricsService` **complements** rather than duplicates auto-instrumentation // MetricsService provides (manually): const entityMetrics = metricsService.createCounter('entities.processed.total'); -entityMetrics.add(entities.length, { operation: 'refresh', kind: 'Component' }); +entityMetrics.add(entities.length, { + operation: 'refresh', + 'entity.kind': 'Component', +}); -// Metric is now available as `backstage.plugin.catalog.entities.processed.total` +// Metric is now available as `entities.processed.total` ``` ### Configuration @@ -162,43 +164,21 @@ interface MetricsService { } ``` -#### Root Metrics Service - -The `RootMetricsService` is responsible for providing metrics to other root services and creating both plugin-scoped and core-scoped `MetricsService` instances. - -```ts -interface RootMetricsService { - // note: no config is provided to the root service. - static forRoot(): RootMetricsService; - forPlugin(pluginId: string): MetricsService; - - // final implementation will be similar to - forService(serviceName: string, scope: 'plugin' | 'core'): MetricsService; -} - -export const rootMetricsServiceFactory = createServiceFactory({ - // depends on as little as possible so that it can be initialized as early as possible. - service: rootMetricsServiceRef, - deps: {}, - factory: () => { - return DefaultRootMetricsService.forRoot(); - }, -}); -``` - #### Plugin Metrics Service -Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions. +Each plugin receives a metrics service that automatically configures the Instrumentation Scope to identify the plugin. The scope name follows the pattern `backstage-plugin-{pluginId}`. ```ts -const metricsServiceFactory = createServiceFactory({ - service: metricsServiceRef, +export const metricsServiceFactory = createServiceFactory({ + service: coreServices.metrics, deps: { - rootMetrics: coreServices.rootMetrics, pluginMetadata: coreServices.pluginMetadata, }, - factory: ({ rootMetrics, pluginMetadata }) => { - return rootMetrics.forPlugin(pluginMetadata.getId()); + factory: ({ pluginMetadata }) => { + const pluginId = pluginMetadata.getId(); + const scopeName = `backstage-plugin-${pluginId}`; + + return new DefaultMetricsService(scopeName, version, ...); }, }); ``` @@ -248,3 +228,22 @@ entitiesProcessed.add(100); - Plugin authors continue to implement their own metrics as they see fit. - A combined TelemetryService that provides both metrics and tracing. + +### Rejected: Forced Namespace Prefixes + +Prepend `backstage.plugin.{pluginId}.` to all metric names. This was the original proposal but conflicts with OpenTelemetry semantic conventions. + +**Problems:** + +- Makes it impossible to use standard semantic conventions like `mcp.*`, `gen_ai.*`, `http.*` +- Breaks compatibility with industry-standard observability tooling +- Prevents cross-service metric aggregation +- Goes against OpenTelemetry best practices and official guidance + +**Example of conflict:** + +```ts +// Plugin wants to emit: mcp.client.operation.duration +// Framework forces: backstage.plugin.mcp-actions.mcp.client.operation.duration +// This violates the semantic convention and breaks tooling +``` diff --git a/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts b/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts index 44119947e6..dea29a9f54 100644 --- a/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts +++ b/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts @@ -142,9 +142,13 @@ export class ReadWriteSplitCatalogService implements CatalogService { } async getLocations( - options: CatalogRequestOptions, + request: {} | undefined, + options: CatalogServiceRequestOptions, ): Promise { - return this.#catalogRead.getLocations(options); + return this.#catalogRead.getLocations( + request, + await this.#getOptions(options), + ); } async getLocationById( diff --git a/contrib/docs/tutorials/devcontainer.md b/contrib/docs/tutorials/devcontainer.md new file mode 100644 index 0000000000..00027de289 --- /dev/null +++ b/contrib/docs/tutorials/devcontainer.md @@ -0,0 +1,199 @@ +# Developing Backstage with Dev Containers + +This guide will help you get started with developing Backstage using [Dev Containers](https://containers.dev/). Dev +Containers provide a consistent, containerized development environment that works across different machines and +operating systems. + +## What are Dev Containers? + +Dev Containers (Development Containers) allow you to use a Docker container as a full-featured development environment. +All dependencies, tools, and configurations are defined in the container, so you don't need to install Node.js, Python, +or other tools on your local machine. + +## Prerequisites + +Before you begin, make sure you have the following installed: + +1. **Docker Desktop** (or Docker Engine on Linux) + +- [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) +- [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) +- [Docker Engine for Linux](https://docs.docker.com/engine/install/) + +2. **Visual Studio Code (or IntelliJ IDEA Ultimate)** + +- [Download VS Code](https://code.visualstudio.com/) +- [Download IntelliJ IDEA](https://www.jetbrains.com/idea/download/) + +**Note**: IntelliJ IDEA Community Edition does not support Dev Containers, so you will need the Ultimate (paid) edition +to use this feature. + +3. **Dev Containers Extension** + +**For VS Code**: + +- Install from + the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) +- Or open VS Code and search for "Dev Containers" in the Extensions view (`Ctrl/Cmd + Shift + X`) + +**For IntelliJ IDEA**: + +- Dev Containers support is available in IntelliJ IDEA Ultimate edition out of the box, + no additional plugin is required + +### System Requirements + +The dev container requires: + +- **CPU**: At least 2 cores +- **Memory**: At least 4GB of RAM +- **Storage**: At least 32GB of free disk space + +## Getting Started + +### Option 1: Open the Repository in a Dev Container + +1. **Fork and clone the Backstage repository** (if you haven't already): + +- Go to https://github.com/backstage/backstage and click "Fork" +- Clone your fork: + + ```shell + git clone https://github.com//backstage.git + cd backstage + ``` + +2. **Open in VS Code**: + +```shell +code . +``` + +3. **Reopen in Container**: + +- VS Code should detect the `.devcontainer` folder and show a notification asking if you want to reopen the folder in + a container +- Click "Reopen in Container" +- Alternatively, open the Command Palette (`Ctrl/Cmd + Shift + P`) and run "Dev Containers: Reopen in Container" + +4. **Wait for the container to build**: + +- The first time you open the container, it will take several minutes to build the Docker image and install all + dependencies +- VS Code will show progress in the bottom-right corner +- The setup script will automatically run `yarn install` and install Python dependencies for TechDocs + +5. **You're ready to develop!** + +- Once the setup is complete, you'll see a welcome message in the terminal +- Open a new terminal in VS Code (`Ctrl/Cmd + ~`) and run `yarn start` to launch Backstage + +### Option 2: Build and Open from Command Palette + +1. Open VS Code with the Backstage repository +2. Open the Command Palette (`Ctrl/Cmd + Shift + P`) +3. Run "Dev Containers: Rebuild and Reopen in Container" + +## What's Included + +The dev container comes pre-configured with: + +### Tools and Runtime + +- **Node.js**: Version suitable for Backstage development +- **Python 3**: For building and previewing TechDocs +- **Yarn**: Package manager +- **Git**: Version control +- **Chromium**: For running end-to-end tests +- **MkDocs with TechDocs core**: For TechDocs development + +### VS Code Extensions + +The following extensions are automatically installed: + +- **Prettier**: Code formatter (format on save enabled) +- **ESLint**: JavaScript/TypeScript linter +- **Backstage**: Official Backstage extension for syntax highlighting and snippets + +### Port Forwarding + +The following ports are automatically forwarded to your local machine: + +- **3000**: Frontend application +- **7007**: Backend API + +Additional ports, such as **9464** for the metrics endpoint, can be forwarded manually from the Ports panel in your +editor, or may be auto-forwarded when detected. + +## Using the Dev Container + +### Starting Backstage + +Once the container is ready and dependencies are installed: + +```shell +yarn start +``` + +This will start both the frontend (on port 3000) and backend (on port 7007). Access Backstage at: + +- **Frontend**: http://localhost:3000 +- **Backend API**: http://localhost:7007 + +> **Note**: The first time you start Backstage, the backend may take a minute to fully start. You might need to refresh +> your browser once the backend is ready. + +## Troubleshooting + +### Container build fails + +If the container fails to build, try: + +1. Make sure Docker Desktop is running +2. Rebuild the container: Command Palette → "Dev Containers: Rebuild Container" +3. Check Docker has enough resources allocated (CPU, memory, disk space) + +### Port already in use + +If ports 3000 or 7007 are already in use on your local machine: + +1. Stop any other Backstage instances or services using those ports +2. Reload the VS Code window: Command Palette → "Developer: Reload Window" + +### Dependencies not installed + +If `yarn start` fails with missing dependencies: + +1. Run `yarn install` manually in the terminal +2. Or rebuild the container to run the setup script again + +### Performance issues + +If the dev container feels slow: + +1. Increase Docker Desktop's resource allocation (Settings → Resources) +2. Make sure your Docker disk image isn't full +3. On Windows, ensure WSL 2 is enabled for better performance + +### Cannot connect to backend + +If the frontend shows connection errors: + +1. Wait a minute for the backend to fully start (check terminal logs) +2. Ensure port 7007 is properly forwarded (check Ports panel in VS Code) +3. Refresh your browser + +## Additional Resources + +- [Dev Containers Documentation](https://containers.dev/) +- [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) +- [Backstage Getting Started Guide](https://backstage.io/docs/getting-started/) +- [Backstage Contributing Guide](../../../CONTRIBUTING.md) + +## Need Help? + +If you encounter issues not covered in this guide: + +- Check the [Backstage Discord](https://discord.gg/backstage) community +- Search or create an issue on [GitHub](https://github.com/backstage/backstage/issues) +- Review the [Backstage documentation](https://backstage.io/docs/) diff --git a/docker-compose.deps.yml b/docker-compose.deps.yml index fa7937fdf1..5e2e3cd5b6 100644 --- a/docker-compose.deps.yml +++ b/docker-compose.deps.yml @@ -2,7 +2,7 @@ name: backstage services: psql: - image: postgres:17.7 + image: postgres:17.9 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs-ui/package.json b/docs-ui/package.json index bb0b1a393d..5cb0aa3e44 100644 --- a/docs-ui/package.json +++ b/docs-ui/package.json @@ -3,16 +3,12 @@ "version": "0.1.0", "private": true, "scripts": { - "prebuild": "yarn sync:css", "build": "next build", "lint": "eslint .", - "prestart": "yarn sync:css", "start": "next dev", "sync:changelog": "node scripts/sync-changelog.mjs", "sync:changelog:dry-run": "node scripts/sync-changelog.mjs --dry-run", - "sync:changelog:force": "node scripts/sync-changelog.mjs --force", - "sync:css": "node scripts/sync-css.js", - "sync:css:watch": "node scripts/sync-css.js --watch" + "sync:changelog:force": "node scripts/sync-changelog.mjs --force" }, "resolutions": { "@types/react": "19.2.10", @@ -46,10 +42,10 @@ "@types/node": "^22.13.14", "@types/react": "19.2.10", "@types/react-dom": "19.2.3", - "chokidar": "^3.6.0", "eslint": "^9", "eslint-config-next": "16.1.6", - "lightningcss": "^1.28.2", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1", "typescript": "^5", "unified": "^11.0.4" } diff --git a/docs-ui/postcss.config.mjs b/docs-ui/postcss.config.mjs new file mode 100644 index 0000000000..bd2bb9bb7c --- /dev/null +++ b/docs-ui/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + 'postcss-import': {}, + }, +}; diff --git a/docs-ui/scripts/sync-css.js b/docs-ui/scripts/sync-css.js deleted file mode 100644 index a9c228ac27..0000000000 --- a/docs-ui/scripts/sync-css.js +++ /dev/null @@ -1,149 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const { bundle } = require('lightningcss'); -const chokidar = require('chokidar'); - -// Configuration -const config = { - UIPath: '../../packages/ui', - outputPath: '../src/css', - files: [ - { - source: 'css/styles.css', - destination: 'theme-backstage.css', - name: 'Main Styles', - }, - { - source: '../../.storybook/themes/spotify.css', - destination: 'theme-spotify.css', - name: 'Spotify Theme', - }, - ], -}; - -class CSSSync { - constructor() { - this.UIPath = path.resolve(__dirname, config.UIPath); - this.outputPath = path.resolve(__dirname, config.outputPath); - this.isWatching = process.argv.includes('--watch'); - } - - async syncFile(fileConfig) { - const sourcePath = path.join(this.UIPath, fileConfig.source); - const destPath = path.join(this.outputPath, fileConfig.destination); - - try { - // Check if source file exists - if (!fs.existsSync(sourcePath)) { - console.warn(`⚠️ Source file not found: ${sourcePath}`); - return false; - } - - // Ensure destination directory exists - fs.mkdirSync(path.dirname(destPath), { recursive: true }); - - // Bundle and optimize CSS - const result = await bundle({ - filename: sourcePath, - minify: true, - }); - - // Write to destination - fs.writeFileSync(destPath, result.code); - - console.log( - `✅ ${fileConfig.name}: ${fileConfig.source} → ${fileConfig.destination}`, - ); - return true; - } catch (error) { - console.error(`❌ Error syncing ${fileConfig.name}:`, error.message); - return false; - } - } - - async syncAll() { - console.log('🔄 Syncing CSS files...\n'); - - let successCount = 0; - for (const fileConfig of config.files) { - if (await this.syncFile(fileConfig)) { - successCount++; - } - } - - console.log( - `\n✨ Synced ${successCount}/${config.files.length} CSS files successfully!`, - ); - - if (successCount > 0) { - console.log('\n📁 Available CSS files in src/css/:'); - config.files.forEach(file => { - const destPath = path.join(this.outputPath, file.destination); - if (fs.existsSync(destPath)) { - const stats = fs.statSync(destPath); - const size = (stats.size / 1024).toFixed(2); - console.log(` • ${file.destination} (${size} KB)`); - } - }); - } - } - - startWatching() { - console.log('👀 Watching for CSS changes...\n'); - - // Watch all source files - const watchPaths = config.files.map(file => - path.join(this.UIPath, file.source), - ); - - const watcher = chokidar.watch(watchPaths, { - ignored: /node_modules/, - persistent: true, - }); - - watcher.on('change', async filePath => { - console.log( - `\n🔄 Change detected: ${path.relative(this.UIPath, filePath)}`, - ); - - // Find which file changed and sync it - const fileConfig = config.files.find(file => - filePath.endsWith(file.source.replace(/\//g, path.sep)), - ); - - if (fileConfig) { - await this.syncFile(fileConfig); - } - }); - - watcher.on('error', error => console.error('❌ Watch error:', error)); - - // Handle process termination - process.on('SIGINT', () => { - console.log('\n👋 Stopping CSS sync...'); - watcher.close(); - process.exit(0); - }); - } - - async run() { - console.log('🎨 BUI CSS Sync Tool\n'); - console.log(`📂 BUI path: ${this.UIPath}`); - console.log(`📂 Output path: ${this.outputPath}\n`); - - // Initial sync - await this.syncAll(); - - // Watch for changes if requested - if (this.isWatching) { - this.startWatching(); - } - } -} - -// Run the sync tool -const cssSync = new CSSSync(); -cssSync.run().catch(error => { - console.error('❌ CSS Sync failed:', error); - process.exit(1); -}); diff --git a/docs-ui/src/app/components/button-link/props-definition.tsx b/docs-ui/src/app/components/button-link/props-definition.tsx index deae5364df..e85a363b5a 100644 --- a/docs-ui/src/app/components/button-link/props-definition.tsx +++ b/docs-ui/src/app/components/button-link/props-definition.tsx @@ -62,6 +62,11 @@ export const buttonLinkPropDefs: Record = { ), }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, onSurface: { type: 'enum', values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'], diff --git a/docs-ui/src/app/components/card/components.tsx b/docs-ui/src/app/components/card/components.tsx index d10e4d7bce..c4d1f809c2 100644 --- a/docs-ui/src/app/components/card/components.tsx +++ b/docs-ui/src/app/components/card/components.tsx @@ -7,6 +7,8 @@ import { CardFooter, } from '../../../../../packages/ui/src/components/Card/Card'; import { Text } from '../../../../../packages/ui/src/components/Text/Text'; +import { Button } from '../../../../../packages/ui/src/components/Button/Button'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; export const Default = () => { return ( @@ -32,6 +34,70 @@ export const HeaderAndBody = () => { ); }; +export const InteractiveButton = () => { + return ( + {}} + label="View component details" + > + Interactive Card + + Click anywhere on this card to trigger the press handler. + + + + Click to interact + + + + ); +}; + +export const InteractiveLink = () => { + return ( + + Link Card + This card navigates to a URL when clicked. + + + Opens backstage.io + + + + ); +}; + +export const InteractiveWithNestedButtons = () => { + return ( + {}} + label="View plugin details" + > + Card with Actions + + Clicking the card background triggers the card press handler. The + buttons below remain independently interactive. + + + + + + + + + ); +}; + export const WithLongBody = () => { return ( diff --git a/docs-ui/src/app/components/card/page.mdx b/docs-ui/src/app/components/card/page.mdx index 6f410fcd97..8df6eccb40 100644 --- a/docs-ui/src/app/components/card/page.mdx +++ b/docs-ui/src/app/components/card/page.mdx @@ -12,8 +12,18 @@ import { defaultSnippet, headerAndBodySnippet, withLongBodySnippet, + interactiveButtonSnippet, + interactiveLinkSnippet, + interactiveWithNestedButtonsSnippet, } from './snippets'; -import { Default, HeaderAndBody, WithLongBody } from './components'; +import { + Default, + HeaderAndBody, + WithLongBody, + InteractiveButton, + InteractiveLink, + InteractiveWithNestedButtons, +} from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; import { CardDefinition } from '../../../utils/definitions'; @@ -81,6 +91,49 @@ When body content exceeds the available height, CardBody scrolls while header an code={withLongBodySnippet} /> +## Interactive cards + +Cards can be made interactive without wrapping the entire card in a button or link — which would conflict with any interactive elements inside. Instead, a transparent overlay covers the card surface, and nested buttons and links remain independently clickable above it. + +### Button + +Pass `onPress` and a `label` (used as the accessible name for screen readers) to make the whole card surface pressable. + +} + code={interactiveButtonSnippet} +/> + +### Link + +Pass `href` to make the card surface navigate to a URL. `label` is required — it provides the accessible name for the invisible overlay link read by screen readers. + +} + code={interactiveLinkSnippet} +/> + +### With nested buttons + +Buttons and links inside the card remain independently interactive. Clicking them does not trigger the card's `onPress` handler. + +} + code={interactiveWithNestedButtonsSnippet} +/> + diff --git a/docs-ui/src/app/components/card/props-definition.ts b/docs-ui/src/app/components/card/props-definition.ts index b3e3776648..7e68d890b6 100644 --- a/docs-ui/src/app/components/card/props-definition.ts +++ b/docs-ui/src/app/components/card/props-definition.ts @@ -15,6 +15,25 @@ const optionalChildrenPropDef: Record = { export const cardPropDefs: Record = { ...optionalChildrenPropDef, + onPress: { + type: 'enum', + values: ['() => void'], + responsive: false, + description: + 'Handler called when the card is pressed. Makes the card interactive as a button. Requires label.', + }, + href: { + type: 'string', + responsive: false, + description: + 'URL to navigate to. Makes the card interactive as a link. Mutually exclusive with onPress.', + }, + label: { + type: 'string', + responsive: false, + description: + 'Accessible label announced by screen readers for the interactive overlay. Required when onPress or href is provided.', + }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/docs-ui/src/app/components/card/snippets.ts b/docs-ui/src/app/components/card/snippets.ts index 6f956e5518..86b9df5e0a 100644 --- a/docs-ui/src/app/components/card/snippets.ts +++ b/docs-ui/src/app/components/card/snippets.ts @@ -17,6 +17,50 @@ export const headerAndBodySnippet = `Body content without a footer `; +export const interactiveButtonSnippet = ` console.log('Card pressed')} + label="View component details" +> + Interactive Card + Click anywhere on this card to trigger the press handler. + Click to interact +`; + +export const interactiveLinkSnippet = ` + Link Card + This card navigates to a URL when clicked. + Opens backstage.io +`; + +export const interactiveWithNestedButtonsSnippet = `import { Button, Flex } from '@backstage/ui'; + + console.log('Card pressed')} + label="View plugin details" +> + Card with Actions + + Clicking the card background triggers the card press handler. + The buttons below remain independently interactive. + + + + + + + +`; + export const withLongBodySnippet = `import { Text } from '@backstage/ui'; diff --git a/docs-ui/src/app/components/header-page/components.tsx b/docs-ui/src/app/components/header/components.tsx similarity index 84% rename from docs-ui/src/app/components/header-page/components.tsx rename to docs-ui/src/app/components/header/components.tsx index 791afc5bbe..dc32bdd6cb 100644 --- a/docs-ui/src/app/components/header-page/components.tsx +++ b/docs-ui/src/app/components/header/components.tsx @@ -1,6 +1,6 @@ 'use client'; -import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage'; +import { Header } from '../../../../../packages/ui/src/components/Header/Header'; import { Button } from '../../../../../packages/ui/src/components/Button/Button'; import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon'; import { @@ -31,7 +31,7 @@ const breadcrumbs = [ export const WithEverything = () => ( - ( export const WithLongBreadcrumbs = () => ( - +
); export const WithTabs = () => ( - +
); export const WithCustomActions = () => ( - Custom action} - /> +
Custom action} /> ); export const WithMenu = () => ( - diff --git a/docs-ui/src/app/components/header-page/page.mdx b/docs-ui/src/app/components/header/page.mdx similarity index 88% rename from docs-ui/src/app/components/header-page/page.mdx rename to docs-ui/src/app/components/header/page.mdx index ac99fb1af0..65dd8f2ea8 100644 --- a/docs-ui/src/app/components/header-page/page.mdx +++ b/docs-ui/src/app/components/header/page.mdx @@ -19,11 +19,11 @@ import { } from './snippets'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; -import { HeaderPageDefinition } from '../../../utils/definitions'; +import { HeaderDefinition } from '../../../utils/definitions'; import { ChangelogComponent } from '@/components/ChangelogComponent'; @@ -61,6 +61,6 @@ Use `customActions` to add a dropdown menu. } code={withMenu} /> - + - + diff --git a/docs-ui/src/app/components/header-page/props-definition.tsx b/docs-ui/src/app/components/header/props-definition.tsx similarity index 97% rename from docs-ui/src/app/components/header-page/props-definition.tsx rename to docs-ui/src/app/components/header/props-definition.tsx index 393141870f..1b7a309b17 100644 --- a/docs-ui/src/app/components/header-page/props-definition.tsx +++ b/docs-ui/src/app/components/header/props-definition.tsx @@ -50,7 +50,7 @@ export const headerPagePropDefs: Record = { type: 'complex', description: 'Breadcrumb trail displayed above the title.', complexType: { - name: 'HeaderPageBreadcrumb[]', + name: 'HeaderBreadcrumb[]', properties: { label: { type: 'string', diff --git a/docs-ui/src/app/components/header-page/snippets.ts b/docs-ui/src/app/components/header/snippets.ts similarity index 78% rename from docs-ui/src/app/components/header-page/snippets.ts rename to docs-ui/src/app/components/header/snippets.ts index 607675717c..8dfef0e69e 100644 --- a/docs-ui/src/app/components/header-page/snippets.ts +++ b/docs-ui/src/app/components/header/snippets.ts @@ -1,8 +1,8 @@ -export const usage = `import { HeaderPage } from '@backstage/ui'; +export const usage = `import { Header } from '@backstage/ui'; -`; +
`; -export const defaultSnippet = ``; -export const withBreadcrumbs = ``; -export const withTabs = ``; -export const withCustomActions = `Custom action} />`; -export const withMenu = ` diff --git a/docs-ui/src/app/components/link/props-definition.tsx b/docs-ui/src/app/components/link/props-definition.tsx index f0aa95114e..951d67e8b4 100644 --- a/docs-ui/src/app/components/link/props-definition.tsx +++ b/docs-ui/src/app/components/link/props-definition.tsx @@ -38,7 +38,7 @@ export const linkPropDefs: Record = { 'body-small', 'body-x-small', ], - default: 'body', + default: 'body-medium', responsive: true, description: 'Typography style. Title variants for headings, body for paragraph text.', @@ -56,7 +56,7 @@ export const linkPropDefs: Record = { }, color: { type: 'enum', - values: ['primary', 'secondary', 'danger', 'warning', 'success'], + values: ['primary', 'secondary', 'danger', 'warning', 'success', 'info'], default: 'primary', responsive: true, description: @@ -68,6 +68,11 @@ export const linkPropDefs: Record = { 'Truncates text with ellipsis when it overflows its container.', default: 'false', }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, standalone: { type: 'boolean', description: 'Removes underline by default. Underline appears on hover.', diff --git a/docs-ui/src/app/components/list/components.tsx b/docs-ui/src/app/components/list/components.tsx new file mode 100644 index 0000000000..5630027f94 --- /dev/null +++ b/docs-ui/src/app/components/list/components.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { + List, + ListRow, +} from '../../../../../packages/ui/src/components/List/List'; +import { MenuItem } from '../../../../../packages/ui/src/components/Menu/Menu'; +import { + TagGroup, + Tag, +} from '../../../../../packages/ui/src/components/TagGroup/TagGroup'; +import { useState } from 'react'; +import type { Selection } from 'react-aria-components'; +import { + RiJavascriptLine, + RiReactjsLine, + RiShipLine, + RiTerminalLine, + RiCodeLine, + RiDeleteBinLine, + RiEdit2Line, + RiShareBoxLine, +} from '@remixicon/react'; + +const items = [ + { + id: 'react', + label: 'React', + description: 'A JavaScript library for building user interfaces', + icon: , + tags: ['frontend', 'ui'], + }, + { + id: 'typescript', + label: 'TypeScript', + description: 'Typed superset of JavaScript', + icon: , + tags: ['typed', 'js'], + }, + { + id: 'javascript', + label: 'JavaScript', + description: 'The language of the web', + icon: , + tags: ['web'], + }, + { + id: 'rust', + label: 'Rust', + description: 'Systems programming with memory safety', + icon: , + tags: ['systems', 'fast'], + }, + { + id: 'go', + label: 'Go', + description: 'Simple, fast, and reliable', + icon: , + tags: ['backend'], + }, +]; + +const menuItems = ( + <> + }>Edit + }>Share + } color="danger"> + Delete + + +); + +export const Default = () => ( + + {item => ( + + {item.tags.map(tag => ( + {tag} + ))} + + } + > + {item.label} + + )} + +); + +export const WithIcons = () => ( + + {item => ( + + {item.label} + + )} + +); + +export const WithDescription = () => ( + + {item => ( + + {item.label} + + )} + +); + +export const SelectionModeSingle = () => { + const [selected, setSelected] = useState(new Set(['react'])); + + return ( + + {item => {item.label}} + + ); +}; + +export const SelectionModeMultiple = () => { + const [selected, setSelected] = useState( + new Set(['react', 'typescript']), + ); + + return ( + + {item => {item.label}} + + ); +}; + +export const Disabled = () => ( + + {item => {item.label}} + +); diff --git a/docs-ui/src/app/components/list/page.mdx b/docs-ui/src/app/components/list/page.mdx new file mode 100644 index 0000000000..dadeb9c1ee --- /dev/null +++ b/docs-ui/src/app/components/list/page.mdx @@ -0,0 +1,103 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ReactAriaLink } from '@/components/ReactAriaLink'; +import { + Default, + WithIcons, + WithDescription, + SelectionModeSingle, + SelectionModeMultiple, + Disabled, +} from './components'; +import { listPropDefs, listRowPropDefs } from './props-definition'; +import { + usage, + preview, + withIcons, + withDescription, + selectionModeSingle, + selectionModeMultiple, + disabled, +} from './snippets'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ListDefinition, ListRowDefinition } from '../../../utils/definitions'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; + +export const reactAriaUrls = { + gridList: 'https://react-aria.adobe.com/GridList', +}; + + + +} code={preview} /> + +## Usage + + + +## API reference + +### List + +Container for a list of interactive rows. + + + + + +### ListRow + +Individual row within a List. + + + + + +## Examples + +### With icons + +} code={withIcons} /> + +### With description + +} + code={withDescription} +/> + +### Single selection + +} + code={selectionModeSingle} +/> + +### Multiple selection + +} + code={selectionModeMultiple} +/> + +### Disabled items + +} code={disabled} /> + + + + diff --git a/docs-ui/src/app/components/list/props-definition.tsx b/docs-ui/src/app/components/list/props-definition.tsx new file mode 100644 index 0000000000..b021fc6e0a --- /dev/null +++ b/docs-ui/src/app/components/list/props-definition.tsx @@ -0,0 +1,84 @@ +import { + classNamePropDefs, + childrenPropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const listPropDefs: Record = { + items: { + type: 'enum', + values: ['Iterable'], + description: 'Item objects in the collection.', + }, + renderEmptyState: { + type: 'enum', + values: ['(props: GridListRenderProps) => ReactNode'], + description: 'Content to display when the collection is empty.', + }, + selectionMode: { + type: 'enum', + values: ['none', 'single', 'multiple'], + description: 'The type of selection allowed.', + }, + selectedKeys: { + type: 'enum', + values: ['all', 'Iterable'], + description: 'The currently selected keys (controlled).', + }, + defaultSelectedKeys: { + type: 'enum', + values: ['all', 'Iterable'], + description: 'The initial selected keys (uncontrolled).', + }, + disabledKeys: { + type: 'enum', + values: ['Iterable'], + description: 'Keys of items that should be disabled.', + }, + onSelectionChange: { + type: 'enum', + values: ['(keys: Selection) => void'], + description: 'Handler called when the selection changes.', + }, + ...childrenPropDefs, + ...classNamePropDefs, +}; + +export const listRowPropDefs: Record = { + id: { + type: 'string', + description: 'Unique identifier for the row.', + }, + textValue: { + type: 'string', + description: + 'Text value for accessibility. Derived from children if string.', + }, + icon: { + type: 'enum', + values: ['ReactNode'], + description: 'Icon displayed before the row label.', + }, + description: { + type: 'string', + description: 'Secondary description text displayed below the label.', + }, + isDisabled: { + type: 'boolean', + description: 'Whether the row is disabled.', + }, + menuItems: { + type: 'enum', + values: ['ReactNode'], + description: + 'Menu items rendered inside an automatically managed dropdown. Pass MenuItem nodes.', + }, + customActions: { + type: 'enum', + values: ['ReactNode'], + description: + 'Custom action elements displayed on the right side of the row, e.g. tags.', + }, + ...childrenPropDefs, + ...classNamePropDefs, +}; diff --git a/docs-ui/src/app/components/list/snippets.ts b/docs-ui/src/app/components/list/snippets.ts new file mode 100644 index 0000000000..ccaaf7e28d --- /dev/null +++ b/docs-ui/src/app/components/list/snippets.ts @@ -0,0 +1,72 @@ +export const usage = `import { List, ListRow } from '@backstage/ui'; + + + {item => {item.label}} +`; + +export const preview = ` + {item => ( + + {item.tags.map(tag => ( + {tag} + ))} + + } + > + {item.label} + + )} +`; + +export const withIcons = ` + {item => ( + + {item.label} + + )} +`; + +export const withDescription = ` + {item => ( + + {item.label} + + )} +`; + +export const selectionModeSingle = `const [selected, setSelected] = useState(new Set(['react'])); + + + {item => {item.label}} +`; + +export const selectionModeMultiple = `const [selected, setSelected] = useState(new Set(['react', 'typescript'])); + + + {item => {item.label}} +`; + +export const disabled = ` + {item => {item.label}} +`; diff --git a/docs-ui/src/app/components/menu/props-definition.tsx b/docs-ui/src/app/components/menu/props-definition.tsx index 5e11b22737..f6cb88b3e2 100644 --- a/docs-ui/src/app/components/menu/props-definition.tsx +++ b/docs-ui/src/app/components/menu/props-definition.tsx @@ -309,6 +309,11 @@ export const menuItemPropDefs: Record = { type: 'boolean', description: 'Whether the item is disabled.', }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, textValue: { type: 'string', description: 'Text used for typeahead and accessibility.', diff --git a/docs-ui/src/app/components/plugin-header/components.tsx b/docs-ui/src/app/components/plugin-header/components.tsx index b3d831e718..045d0956c4 100644 --- a/docs-ui/src/app/components/plugin-header/components.tsx +++ b/docs-ui/src/app/components/plugin-header/components.tsx @@ -1,7 +1,7 @@ 'use client'; import { PluginHeader } from '../../../../../packages/ui/src/components/PluginHeader/PluginHeader'; -import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage'; +import { Header } from '../../../../../packages/ui/src/components/Header/Header'; import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon'; import { Button } from '../../../../../packages/ui/src/components/Button/Button'; import { MemoryRouter } from 'react-router-dom'; @@ -58,11 +58,11 @@ export const WithAllOptions = () => ( ); -export const WithHeaderPage = () => ( +export const WithHeader = () => ( <> - Custom action} diff --git a/docs-ui/src/app/components/plugin-header/page.mdx b/docs-ui/src/app/components/plugin-header/page.mdx index 5f8e02bfc4..cbe4dfe141 100644 --- a/docs-ui/src/app/components/plugin-header/page.mdx +++ b/docs-ui/src/app/components/plugin-header/page.mdx @@ -4,7 +4,7 @@ import { Snippet } from '@/components/Snippet'; import { WithAllOptionsAndTabs, WithAllOptions, - WithHeaderPage, + WithHeader, } from './components'; import { headerPropDefs } from './props-definition'; import { @@ -12,7 +12,7 @@ import { simple, defaultSnippet, withTabs, - withHeaderPage, + withHeader, } from './snippets'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; @@ -46,11 +46,11 @@ Tabs use React Router and highlight automatically based on the current route. } code={withTabs} open /> -### Plugin header with HeaderPage +### Plugin header with Header -Combine with [HeaderPage](/components/header-page) for multi-level navigation. +Combine with [Header](/components/header) for multi-level navigation. -} code={withHeaderPage} open /> +} code={withHeader} open /> diff --git a/docs-ui/src/app/components/plugin-header/snippets.ts b/docs-ui/src/app/components/plugin-header/snippets.ts index 4e9518af73..874a16184a 100644 --- a/docs-ui/src/app/components/plugin-header/snippets.ts +++ b/docs-ui/src/app/components/plugin-header/snippets.ts @@ -43,7 +43,7 @@ export const withTabs = ``; -export const withHeaderPage = ` - { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + ); +}; + +export const WithRichContent = () => { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + {fruit.description} + + + ))} + + ); +}; + +export const InHeader = () => { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + + } + /> + + ); +}; + +export const WithSelection = () => { + const [inputValue, setInputValue] = useState(''); + const [selected, setSelected] = useState(null); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + + {filtered.map(fruit => ( + { + setSelected(fruit.name); + setInputValue(''); + }} + > + {fruit.name} + + ))} + + Last selected: {selected ?? 'none'} + + ); +}; diff --git a/docs-ui/src/app/components/search-autocomplete/page.mdx b/docs-ui/src/app/components/search-autocomplete/page.mdx new file mode 100644 index 0000000000..ecb58f3377 --- /dev/null +++ b/docs-ui/src/app/components/search-autocomplete/page.mdx @@ -0,0 +1,95 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { ReactAriaLink } from '@/components/ReactAriaLink'; +import { + searchAutocompletePropDefs, + searchAutocompleteItemPropDefs, +} from './props-definition'; +import { + usage, + defaultSnippet, + withRichContent, + inHeader, + withSelection, +} from './snippets'; +import { + WithItems, + WithRichContent, + InHeader, + WithSelection, +} from './components'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; +import { + SearchAutocompleteDefinition, + SearchAutocompleteItemDefinition, +} from '../../../utils/definitions'; + +export const reactAriaUrls = { + autocomplete: 'https://react-aria.adobe.com/Autocomplete', +}; + + + +} code={defaultSnippet} /> + +## Usage + + + +## API reference + +### SearchAutocomplete + + + + + +### SearchAutocompleteItem + +Individual result item within the autocomplete list. + + + +## Examples + +### With rich content + +Here's a view when items include a title and description. + +} + code={withRichContent} +/> + +### In header + +Use `popoverWidth` to control the dropdown width when placed in constrained layouts like `PluginHeader`. + +} code={inHeader} /> + +### With selection + +Use `onAction` on items to handle selection and reset the input. + +} + code={withSelection} +/> + + + + + + diff --git a/docs-ui/src/app/components/search-autocomplete/props-definition.tsx b/docs-ui/src/app/components/search-autocomplete/props-definition.tsx new file mode 100644 index 0000000000..7ddaea61e2 --- /dev/null +++ b/docs-ui/src/app/components/search-autocomplete/props-definition.tsx @@ -0,0 +1,97 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; +import { Chip } from '@/components/Chip'; + +export const searchAutocompletePropDefs: Record = { + 'aria-label': { + type: 'string', + description: 'Accessible label for the search input.', + }, + 'aria-labelledby': { + type: 'string', + description: 'ID of the element that labels the search input.', + }, + inputValue: { + type: 'string', + description: 'The current input value (controlled).', + }, + onInputChange: { + type: 'enum', + values: ['(value: string) => void'], + description: 'Handler called when the input value changes.', + }, + placeholder: { + type: 'string', + default: 'Search', + description: + 'Placeholder text shown when the input is empty. Also used as the accessible label when neither aria-label nor aria-labelledby is provided.', + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + description: ( + <> + Visual size of the input. Use small for inline or dense + layouts, medium for standalone fields. + + ), + }, + isLoading: { + type: 'boolean', + default: 'false', + description: + 'Whether results are currently loading. Dims existing results and announces loading state to screen readers.', + }, + popoverWidth: { + type: 'string', + description: + 'Width of the results popover. Accepts any CSS width value. Matches the input width when not set.', + }, + popoverPlacement: { + type: 'enum', + values: ['bottom start', 'bottom end', 'top start', 'top end'], + default: 'bottom start', + description: 'Placement of the results popover relative to the input.', + }, + defaultOpen: { + type: 'boolean', + default: 'false', + description: 'Whether the results popover is open by default.', + }, + children: { + type: 'enum', + values: ['ReactNode'], + description: 'The result items to render inside the autocomplete.', + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const searchAutocompleteItemPropDefs: Record = { + id: { + type: 'string', + description: 'Unique identifier for the item.', + }, + textValue: { + type: 'string', + description: + 'Plain text value used for keyboard navigation and accessibility.', + }, + onAction: { + type: 'enum', + values: ['() => void'], + description: 'Handler called when the item is selected.', + }, + children: { + type: 'enum', + values: ['ReactNode'], + required: true, + description: 'Content to render inside the item.', + }, + ...classNamePropDefs, +}; diff --git a/docs-ui/src/app/components/search-autocomplete/snippets.ts b/docs-ui/src/app/components/search-autocomplete/snippets.ts new file mode 100644 index 0000000000..fe799ff828 --- /dev/null +++ b/docs-ui/src/app/components/search-autocomplete/snippets.ts @@ -0,0 +1,139 @@ +export const usage = `import { SearchAutocomplete, SearchAutocompleteItem } from '@backstage/ui'; + + + {items.map(item => ( + + {item.name} + + ))} +`; + +export const defaultSnippet = `const fruits = [ + { id: 'apple', name: 'Apple' }, + { id: 'banana', name: 'Banana' }, + { id: 'cherry', name: 'Cherry' }, + { id: 'grape', name: 'Grape' }, + { id: 'orange', name: 'Orange' }, +]; + +function Example() { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + ); +}`; + +export const withRichContent = `const fruits = [ + { id: 'apple', name: 'Apple', description: 'A round fruit' }, + { id: 'banana', name: 'Banana', description: 'A yellow curved fruit' }, + { id: 'cherry', name: 'Cherry', description: 'A small red stone fruit' }, +]; + +function Example() { + const [inputValue, setInputValue] = useState(''); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + {filtered.map(fruit => ( + + {fruit.name} + + {fruit.description} + + + ))} + + ); +}`; + +export const inHeader = ` + + {filtered.map(fruit => ( + + {fruit.name} + + ))} + + + } +/>`; + +export const withSelection = `function Example() { + const [inputValue, setInputValue] = useState(''); + const [selected, setSelected] = useState(null); + + const filtered = fruits.filter(fruit => + fruit.name.toLowerCase().includes(inputValue.toLowerCase()), + ); + + return ( + + + {filtered.map(fruit => ( + { + setSelected(fruit.name); + setInputValue(''); + }} + > + {fruit.name} + + ))} + + Last selected: {selected ?? 'none'} + + ); +}`; diff --git a/docs-ui/src/app/components/table/page.mdx b/docs-ui/src/app/components/table/page.mdx index 6eb280cbe2..8d0e803bd2 100644 --- a/docs-ui/src/app/components/table/page.mdx +++ b/docs-ui/src/app/components/table/page.mdx @@ -42,6 +42,7 @@ import { tableCombinedSnippet, tableCustomRowSnippet, tablePrimitivesSnippet, + tableCellRequirementSnippet, } from './snippets'; import { ChangelogComponent } from '@/components/ChangelogComponent'; import { PageTitle } from '@/components/PageTitle'; @@ -94,6 +95,18 @@ For full control over state, use the controlled props instead: - `search` / `onSearchChange` - `filter` / `onFilterChange` +### Cell Requirement + +Every cell rendered via `ColumnConfig.cell` (or inside a custom `RowRenderFn`) **must** return a cell component as the top-level element. The available cell components are: + +- **`CellText`** — displays a title with optional description and icon. +- **`CellProfile`** — displays an avatar with a name and optional description. +- **`Cell`** — a generic wrapper for fully custom cell content. + +Returning bare text, React fragments, or other elements without wrapping them in one of these cell components will break the table layout. + + + ## Common Patterns ### Sorting diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index be24e86bf0..6d35cb3350 100644 --- a/docs-ui/src/app/components/table/props-definition.tsx +++ b/docs-ui/src/app/components/table/props-definition.tsx @@ -430,6 +430,17 @@ export const tableRootPropDefs: Record = { ), }, + loading: { + type: 'boolean', + default: 'false', + description: ( + <> + Whether the table is in a loading state (e.g., initial data fetch). Adds{' '} + aria-busy attribute and data-loading data + attribute for styling. + + ), + }, }; export const columnPropDefs: Record = { @@ -456,6 +467,11 @@ export const rowPropDefs: Record = { description: 'Row content. Can be a render function receiving column config.', }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/docs-ui/src/app/components/table/snippets.ts b/docs-ui/src/app/components/table/snippets.ts index ab4bf53a58..39f266f2ea 100644 --- a/docs-ui/src/app/components/table/snippets.ts +++ b/docs-ui/src/app/components/table/snippets.ts @@ -57,6 +57,23 @@ const { filter, // { value, onChange } for filters } = useTable({ ... });`; +// ============================================================================= +// Cell Requirement +// ============================================================================= + +export const tableCellRequirementSnippet = `// ✅ Correct — CellText is the top-level element +cell: item => ; + +// ✅ Correct — Cell wraps custom content +cell: item => ( + + + +); + +// ❌ Wrong — bare text without a cell wrapper +cell: item => {item.name};`; + // ============================================================================= // Common Patterns // ============================================================================= diff --git a/docs-ui/src/app/components/tabs/props-definition.ts b/docs-ui/src/app/components/tabs/props-definition.ts index e1c1f056cb..92d1c3d258 100644 --- a/docs-ui/src/app/components/tabs/props-definition.ts +++ b/docs-ui/src/app/components/tabs/props-definition.ts @@ -68,6 +68,11 @@ export const tabPropDefs: Record = { default: 'false', description: 'Disables this tab. Use for temporarily unavailable options.', }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, ...childrenPropDefs, ...classNamePropDefs, }; diff --git a/docs-ui/src/app/components/tag-group/props-definition.tsx b/docs-ui/src/app/components/tag-group/props-definition.tsx index 3f64b03e8c..dc10d5f7e7 100644 --- a/docs-ui/src/app/components/tag-group/props-definition.tsx +++ b/docs-ui/src/app/components/tag-group/props-definition.tsx @@ -84,6 +84,11 @@ export const tagPropDefs: Record = { type: 'boolean', description: 'Whether the tag is disabled.', }, + noTrack: { + type: 'boolean', + description: + 'Suppresses the automatic analytics click event, e.g. if you already have custom tracking.', + }, ...childrenPropDefs, ...classNamePropDefs, }; diff --git a/docs-ui/src/app/get-started/installation/page.mdx b/docs-ui/src/app/get-started/installation/page.mdx index cdb14d5924..d87c2bcba4 100644 --- a/docs-ui/src/app/get-started/installation/page.mdx +++ b/docs-ui/src/app/get-started/installation/page.mdx @@ -1,6 +1,10 @@ import { CodeBlock } from '@/components/CodeBlock'; import { Banner } from '@/components/Banner'; -import { snippet } from './snippets'; +import { + snippet, + analyticsSetupSnippet, + analyticsNoTrackSnippet, +} from './snippets'; # Installation @@ -44,3 +48,30 @@ your plugin and import the components you need. /> + +## BUIProvider + +`BUIProvider` provides routing and analytics integration for all BUI components. It must be rendered inside a React Router context for client-side navigation to work in components like Link, ButtonLink, Tabs, Menu, TagGroup, and Table. + +### Setup + +If you're using the **new frontend system**, the provider is wired automatically via `@backstage/plugin-app` — no setup required. + +For the **old frontend system**, the `BUIProvider` is included in the app shell from `@backstage/core-app-api` and works out of the box. + +If you need to set up the provider manually (e.g. in a custom app shell), wrap your app content with the `BUIProvider` inside your Router and pass in Backstage's `useAnalytics` hook: + + + + + +### Analytics + +Once configured, BUI components with navigation behavior — Link, ButtonLink, Tab, MenuItem, Tag, and Row — fire a `click` event through Backstage's analytics system when a user navigates. Events include the link text as the subject and the destination URL in the attributes, along with any `AnalyticsContext` metadata (such as `pluginId`) from the component's position in the tree. + +To suppress tracking on an individual component, use the `noTrack` prop: + + diff --git a/docs-ui/src/app/get-started/installation/snippets.ts b/docs-ui/src/app/get-started/installation/snippets.ts index 7e63005172..61edb266b3 100644 --- a/docs-ui/src/app/get-started/installation/snippets.ts +++ b/docs-ui/src/app/get-started/installation/snippets.ts @@ -4,3 +4,19 @@ export const snippet = `import { Flex, Button, Text } from '@backstage/ui'; Hello World ;`; + +export const analyticsSetupSnippet = `import { BUIProvider } from '@backstage/ui'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { BrowserRouter } from 'react-router-dom'; + +// BUIProvider must be inside a Router for client-side navigation + + + + +`; + +export const analyticsNoTrackSnippet = `// Suppress analytics for a specific link + + Skip tracking +`; diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx new file mode 100644 index 0000000000..ea73f07ce3 --- /dev/null +++ b/docs-ui/src/app/hooks/page.mdx @@ -0,0 +1,7 @@ +import { HookGrid } from '@/components/HookGrid'; + +# Hooks + +Backstage UI custom hooks provide easy access to design system features, style management, and responsive interface creation. + + diff --git a/docs-ui/src/app/hooks/use-breakpoint/components.tsx b/docs-ui/src/app/hooks/use-breakpoint/components.tsx new file mode 100644 index 0000000000..c58010c66f --- /dev/null +++ b/docs-ui/src/app/hooks/use-breakpoint/components.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useBreakpoint } from '@backstage/ui'; + +export function UseBreakpointExample() { + const { breakpoint, up, down } = useBreakpoint(); + + return ( +
+

Current Breakpoint: {breakpoint}

+ {(up('md') &&

The viewport is larger than 1024px.

) || + (down('sm') &&

The viewport is smaller than 768px.

) || ( +

The viewport is between 768px and 1024px.

+ )} +
+ ); +} diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx new file mode 100644 index 0000000000..f7f3378a41 --- /dev/null +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -0,0 +1,47 @@ +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { PageTitle } from '@/components/PageTitle'; +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { Banner } from '@/components/Banner'; +import { useBreakpointReturnDefs } from './props-definition'; +import { useBreakpointExampleSnippet } from './snippets'; +import { UseBreakpointExample } from './components'; + + + +## Usage + + + +The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively. + +} + code={useBreakpointExampleSnippet} +/> + +## Breakpoints + +The default breakpoints are: + +- **initial**: < 640px +- **xs**: ≥ 640px < 768px +- **sm**: ≥ 768px < 1024px +- **md**: ≥ 1024px < 1280px +- **lg**: ≥ 1280px < 1536px +- **xl**: ≥ 1536px + +## Return Value + + + + diff --git a/docs-ui/src/app/hooks/use-breakpoint/props-definition.ts b/docs-ui/src/app/hooks/use-breakpoint/props-definition.ts new file mode 100644 index 0000000000..b1702a82cc --- /dev/null +++ b/docs-ui/src/app/hooks/use-breakpoint/props-definition.ts @@ -0,0 +1,23 @@ +import { type PropDef } from '@/utils/propDefs'; + +const Breakpoint = ['"initial"', '"xs"', '"sm"', '"md"', '"lg"', '"xl"']; + +export const useBreakpointReturnDefs: Record = { + breakpoint: { + type: 'enum', + values: Breakpoint, + description: 'The current active breakpoint based on screen width', + }, + up: { + type: 'enum', + values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`], + description: + 'Function that takes a breakpoint and returns true if the screen width is at or above that breakpoint', + }, + down: { + type: 'enum', + values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`], + description: + 'Function that takes a breakpoint and returns true if the screen width is at or below that breakpoint', + }, +}; diff --git a/docs-ui/src/app/hooks/use-breakpoint/snippets.ts b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts new file mode 100644 index 0000000000..19e6debe65 --- /dev/null +++ b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts @@ -0,0 +1,13 @@ +export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui'; + +function ResponsiveComponent() { + const { breakpoint, up, down } = useBreakpoint(); + + return ( +
+

Current Breakpoint: {breakpoint}

+ {up('md') &&

The viewport is medium or larger.

} + {down('sm') &&

The viewport is small or smaller.

} +
+ ); +}`; diff --git a/docs-ui/src/app/layout.tsx b/docs-ui/src/app/layout.tsx index a0f8ae5816..015ee8346c 100644 --- a/docs-ui/src/app/layout.tsx +++ b/docs-ui/src/app/layout.tsx @@ -49,10 +49,6 @@ export default async function RootLayout({ data-theme-name="backstage" suppressHydrationWarning > - - - - diff --git a/docs-ui/src/app/snippets.ts b/docs-ui/src/app/snippets.ts index e16463a085..4a93c31831 100644 --- a/docs-ui/src/app/snippets.ts +++ b/docs-ui/src/app/snippets.ts @@ -1,19 +1,19 @@ export const surfacesSnippet = ` - + - + `; -export const adaptiveSnippet = ` +export const adaptiveSnippet = ` {/* automatically set background to neutral-2 */} `; -export const customCardSnippet = ` +export const customCardSnippet = ` Hello World `; diff --git a/docs-ui/src/components/Banner/styles.module.css b/docs-ui/src/components/Banner/styles.module.css index 9666c5d521..d9d3323376 100644 --- a/docs-ui/src/components/Banner/styles.module.css +++ b/docs-ui/src/components/Banner/styles.module.css @@ -16,13 +16,13 @@ .info { background-color: #f2f2f2; border-color: #cdcdcd; - color: #888888; + color: #4b4b4b; } .warning { background-color: #fff2b9; - border-color: #ffd000; - color: #d79927; + border-color: #d79927; + color: #8a3d09; } .icon { diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx index 92068d04f8..cca75a9515 100644 --- a/docs-ui/src/components/ChangelogComponent/index.tsx +++ b/docs-ui/src/components/ChangelogComponent/index.tsx @@ -1,16 +1,24 @@ import { changelog } from '@/utils/changelog'; import { MDXRemote } from 'next-mdx-remote-client/rsc'; import { formattedMDXComponents } from '@/mdx-components'; -import type { Component } from '@/utils/changelog'; +import type { AtLeastOne, Component, Hook } from '@/utils/changelog'; import { Badge, BreakingBadge, generateChangelogMarkdown, } from '../Changelog/utils'; -export const ChangelogComponent = ({ component }: { component: Component }) => { - const componentChangelog = changelog.filter(c => - c.components.includes(component), +type ChangelogComponentProps = AtLeastOne<{ + component: Component; + hook: Hook; +}>; + +export const ChangelogComponent = ({ + component, + hook, +}: Readonly) => { + const componentChangelog = changelog.filter( + c => c.components?.includes(component) || c.hooks?.includes(hook), ); const content = `## Changelog diff --git a/docs-ui/src/components/HookGrid/HookGrid.module.css b/docs-ui/src/components/HookGrid/HookGrid.module.css new file mode 100644 index 0000000000..a6dd54a864 --- /dev/null +++ b/docs-ui/src/components/HookGrid/HookGrid.module.css @@ -0,0 +1,29 @@ +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + margin-top: 1rem; + margin-bottom: 2rem; +} + +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.item { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 0; + font-size: 14px; + font-weight: 400; + color: var(--primary); + text-decoration: none; + + &:hover { + color: var(--link); + text-decoration: underline; + text-underline-offset: 2px; + } +} diff --git a/docs-ui/src/components/HookGrid/HookGrid.tsx b/docs-ui/src/components/HookGrid/HookGrid.tsx new file mode 100644 index 0000000000..871ee885d2 --- /dev/null +++ b/docs-ui/src/components/HookGrid/HookGrid.tsx @@ -0,0 +1,21 @@ +'use client'; + +import Link from 'next/link'; +import { hooks } from '@/utils/data'; +import styles from './HookGrid.module.css'; + +export const HookGrid = () => { + return ( +
+ {hooks.map(item => ( + + {item.title} + + ))} +
+ ); +}; diff --git a/docs-ui/src/components/HookGrid/index.ts b/docs-ui/src/components/HookGrid/index.ts new file mode 100644 index 0000000000..934f73aacf --- /dev/null +++ b/docs-ui/src/components/HookGrid/index.ts @@ -0,0 +1 @@ +export * from './HookGrid'; diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx index bad77e942e..d27303d94b 100644 --- a/docs-ui/src/components/Navigation/Navigation.tsx +++ b/docs-ui/src/components/Navigation/Navigation.tsx @@ -4,14 +4,14 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import clsx from 'clsx'; import { - RiCollageLine, + RiCodeSSlashLine, RiFileHistoryLine, RiHazeLine, RiPaletteLine, RiServiceLine, RiStackLine, } from '@remixicon/react'; -import { components } from '@/utils/data'; +import { components, hooks } from '@/utils/data'; import styles from './Navigation.module.css'; interface NavigationProps { @@ -66,7 +66,7 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
- + Components
{components.map(item => { @@ -92,6 +92,29 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => { ); })} +
+ Hooks +
+ {hooks.map(item => { + const isActive = pathname === `/hooks/${item.slug}`; + return ( + +
{item.title}
+
+ {item.status === 'alpha' && 'Alpha'} + {item.status === 'beta' && 'Beta'} + {item.status === 'inProgress' && 'In Progress'} + {item.status === 'stable' && 'Stable'} + {item.status === 'deprecated' && 'Deprecated'} +
+ + ); + })} ); }; diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx index 4a071bf45e..ea094b2a81 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.tsx +++ b/docs-ui/src/components/Toolbar/Toolbar.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { RiArrowDownSLine, + RiDiscordLine, RiGithubLine, RiMoonLine, RiSearchLine, @@ -95,6 +96,7 @@ export const Toolbar = ({ version }: ToolbarProps) => { @@ -103,11 +105,23 @@ export const Toolbar = ({ version }: ToolbarProps) => { + + + =640px) { - .xs\:bui-p { - padding: var(--p-xs); - } - .xs\:bui-p-0\.5 { - padding: var(--bui-space-0_5); - } - .xs\:bui-p-1 { - padding: var(--bui-space-1); - } - .xs\:bui-p-1\.5 { - padding: var(--bui-space-1_5); - } - .xs\:bui-p-2 { - padding: var(--bui-space-2); - } - .xs\:bui-p-3 { - padding: var(--bui-space-3); - } - .xs\:bui-p-4 { - padding: var(--bui-space-4); - } - .xs\:bui-p-5 { - padding: var(--bui-space-5); - } - .xs\:bui-p-6 { - padding: var(--bui-space-6); - } - .xs\:bui-p-7 { - padding: var(--bui-space-7); - } - .xs\:bui-p-8 { - padding: var(--bui-space-8); - } - .xs\:bui-p-9 { - padding: var(--bui-space-9); - } - .xs\:bui-p-10 { - padding: var(--bui-space-10); - } - .xs\:bui-p-11 { - padding: var(--bui-space-11); - } - .xs\:bui-p-12 { - padding: var(--bui-space-12); - } - .xs\:bui-p-13 { - padding: var(--bui-space-13); - } - .xs\:bui-p-14 { - padding: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-p { - padding: var(--p-sm); - } - .sm\:bui-p-0\.5 { - padding: var(--bui-space-0_5); - } - .sm\:bui-p-1 { - padding: var(--bui-space-1); - } - .sm\:bui-p-1\.5 { - padding: var(--bui-space-1_5); - } - .sm\:bui-p-2 { - padding: var(--bui-space-2); - } - .sm\:bui-p-3 { - padding: var(--bui-space-3); - } - .sm\:bui-p-4 { - padding: var(--bui-space-4); - } - .sm\:bui-p-5 { - padding: var(--bui-space-5); - } - .sm\:bui-p-6 { - padding: var(--bui-space-6); - } - .sm\:bui-p-7 { - padding: var(--bui-space-7); - } - .sm\:bui-p-8 { - padding: var(--bui-space-8); - } - .sm\:bui-p-9 { - padding: var(--bui-space-9); - } - .sm\:bui-p-10 { - padding: var(--bui-space-10); - } - .sm\:bui-p-11 { - padding: var(--bui-space-11); - } - .sm\:bui-p-12 { - padding: var(--bui-space-12); - } - .sm\:bui-p-13 { - padding: var(--bui-space-13); - } - .sm\:bui-p-14 { - padding: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-p { - padding: var(--p-md); - } - .md\:bui-p-0\.5 { - padding: var(--bui-space-0_5); - } - .md\:bui-p-1 { - padding: var(--bui-space-1); - } - .md\:bui-p-1\.5 { - padding: var(--bui-space-1_5); - } - .md\:bui-p-2 { - padding: var(--bui-space-2); - } - .md\:bui-p-3 { - padding: var(--bui-space-3); - } - .md\:bui-p-4 { - padding: var(--bui-space-4); - } - .md\:bui-p-5 { - padding: var(--bui-space-5); - } - .md\:bui-p-6 { - padding: var(--bui-space-6); - } - .md\:bui-p-7 { - padding: var(--bui-space-7); - } - .md\:bui-p-8 { - padding: var(--bui-space-8); - } - .md\:bui-p-9 { - padding: var(--bui-space-9); - } - .md\:bui-p-10 { - padding: var(--bui-space-10); - } - .md\:bui-p-11 { - padding: var(--bui-space-11); - } - .md\:bui-p-12 { - padding: var(--bui-space-12); - } - .md\:bui-p-13 { - padding: var(--bui-space-13); - } - .md\:bui-p-14 { - padding: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-p { - padding: var(--p-lg); - } - .lg\:bui-p-0\.5 { - padding: var(--bui-space-0_5); - } - .lg\:bui-p-1 { - padding: var(--bui-space-1); - } - .lg\:bui-p-1\.5 { - padding: var(--bui-space-1_5); - } - .lg\:bui-p-2 { - padding: var(--bui-space-2); - } - .lg\:bui-p-3 { - padding: var(--bui-space-3); - } - .lg\:bui-p-4 { - padding: var(--bui-space-4); - } - .lg\:bui-p-5 { - padding: var(--bui-space-5); - } - .lg\:bui-p-6 { - padding: var(--bui-space-6); - } - .lg\:bui-p-7 { - padding: var(--bui-space-7); - } - .lg\:bui-p-8 { - padding: var(--bui-space-8); - } - .lg\:bui-p-9 { - padding: var(--bui-space-9); - } - .lg\:bui-p-10 { - padding: var(--bui-space-10); - } - .lg\:bui-p-11 { - padding: var(--bui-space-11); - } - .lg\:bui-p-12 { - padding: var(--bui-space-12); - } - .lg\:bui-p-13 { - padding: var(--bui-space-13); - } - .lg\:bui-p-14 { - padding: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-p { - padding: var(--p-xl); - } - .xl\:bui-p-0\.5 { - padding: var(--bui-space-0_5); - } - .xl\:bui-p-1 { - padding: var(--bui-space-1); - } - .xl\:bui-p-1\.5 { - padding: var(--bui-space-1_5); - } - .xl\:bui-p-2 { - padding: var(--bui-space-2); - } - .xl\:bui-p-3 { - padding: var(--bui-space-3); - } - .xl\:bui-p-4 { - padding: var(--bui-space-4); - } - .xl\:bui-p-5 { - padding: var(--bui-space-5); - } - .xl\:bui-p-6 { - padding: var(--bui-space-6); - } - .xl\:bui-p-7 { - padding: var(--bui-space-7); - } - .xl\:bui-p-8 { - padding: var(--bui-space-8); - } - .xl\:bui-p-9 { - padding: var(--bui-space-9); - } - .xl\:bui-p-10 { - padding: var(--bui-space-10); - } - .xl\:bui-p-11 { - padding: var(--bui-space-11); - } - .xl\:bui-p-12 { - padding: var(--bui-space-12); - } - .xl\:bui-p-13 { - padding: var(--bui-space-13); - } - .xl\:bui-p-14 { - padding: var(--bui-space-14); - } - } - .bui-pl { - padding-left: var(--pl); - } - .bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .bui-pl-1 { - padding-left: var(--bui-space-1); - } - .bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .bui-pl-2 { - padding-left: var(--bui-space-2); - } - .bui-pl-3 { - padding-left: var(--bui-space-3); - } - .bui-pl-4 { - padding-left: var(--bui-space-4); - } - .bui-pl-5 { - padding-left: var(--bui-space-5); - } - .bui-pl-6 { - padding-left: var(--bui-space-6); - } - .bui-pl-7 { - padding-left: var(--bui-space-7); - } - .bui-pl-8 { - padding-left: var(--bui-space-8); - } - .bui-pl-9 { - padding-left: var(--bui-space-9); - } - .bui-pl-10 { - padding-left: var(--bui-space-10); - } - .bui-pl-11 { - padding-left: var(--bui-space-11); - } - .bui-pl-12 { - padding-left: var(--bui-space-12); - } - .bui-pl-13 { - padding-left: var(--bui-space-13); - } - .bui-pl-14 { - padding-left: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-pl { - padding-left: var(--pl-xs); - } - .xs\:bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .xs\:bui-pl-1 { - padding-left: var(--bui-space-1); - } - .xs\:bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .xs\:bui-pl-2 { - padding-left: var(--bui-space-2); - } - .xs\:bui-pl-3 { - padding-left: var(--bui-space-3); - } - .xs\:bui-pl-4 { - padding-left: var(--bui-space-4); - } - .xs\:bui-pl-5 { - padding-left: var(--bui-space-5); - } - .xs\:bui-pl-6 { - padding-left: var(--bui-space-6); - } - .xs\:bui-pl-7 { - padding-left: var(--bui-space-7); - } - .xs\:bui-pl-8 { - padding-left: var(--bui-space-8); - } - .xs\:bui-pl-9 { - padding-left: var(--bui-space-9); - } - .xs\:bui-pl-10 { - padding-left: var(--bui-space-10); - } - .xs\:bui-pl-11 { - padding-left: var(--bui-space-11); - } - .xs\:bui-pl-12 { - padding-left: var(--bui-space-12); - } - .xs\:bui-pl-13 { - padding-left: var(--bui-space-13); - } - .xs\:bui-pl-14 { - padding-left: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-pl { - padding-left: var(--pl-sm); - } - .sm\:bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .sm\:bui-pl-1 { - padding-left: var(--bui-space-1); - } - .sm\:bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .sm\:bui-pl-2 { - padding-left: var(--bui-space-2); - } - .sm\:bui-pl-3 { - padding-left: var(--bui-space-3); - } - .sm\:bui-pl-4 { - padding-left: var(--bui-space-4); - } - .sm\:bui-pl-5 { - padding-left: var(--bui-space-5); - } - .sm\:bui-pl-6 { - padding-left: var(--bui-space-6); - } - .sm\:bui-pl-7 { - padding-left: var(--bui-space-7); - } - .sm\:bui-pl-8 { - padding-left: var(--bui-space-8); - } - .sm\:bui-pl-9 { - padding-left: var(--bui-space-9); - } - .sm\:bui-pl-10 { - padding-left: var(--bui-space-10); - } - .sm\:bui-pl-11 { - padding-left: var(--bui-space-11); - } - .sm\:bui-pl-12 { - padding-left: var(--bui-space-12); - } - .sm\:bui-pl-13 { - padding-left: var(--bui-space-13); - } - .sm\:bui-pl-14 { - padding-left: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-pl { - padding-left: var(--pl-md); - } - .md\:bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .md\:bui-pl-1 { - padding-left: var(--bui-space-1); - } - .md\:bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .md\:bui-pl-2 { - padding-left: var(--bui-space-2); - } - .md\:bui-pl-3 { - padding-left: var(--bui-space-3); - } - .md\:bui-pl-4 { - padding-left: var(--bui-space-4); - } - .md\:bui-pl-5 { - padding-left: var(--bui-space-5); - } - .md\:bui-pl-6 { - padding-left: var(--bui-space-6); - } - .md\:bui-pl-7 { - padding-left: var(--bui-space-7); - } - .md\:bui-pl-8 { - padding-left: var(--bui-space-8); - } - .md\:bui-pl-9 { - padding-left: var(--bui-space-9); - } - .md\:bui-pl-10 { - padding-left: var(--bui-space-10); - } - .md\:bui-pl-11 { - padding-left: var(--bui-space-11); - } - .md\:bui-pl-12 { - padding-left: var(--bui-space-12); - } - .md\:bui-pl-13 { - padding-left: var(--bui-space-13); - } - .md\:bui-pl-14 { - padding-left: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-pl { - padding-left: var(--pl-lg); - } - .lg\:bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .lg\:bui-pl-1 { - padding-left: var(--bui-space-1); - } - .lg\:bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .lg\:bui-pl-2 { - padding-left: var(--bui-space-2); - } - .lg\:bui-pl-3 { - padding-left: var(--bui-space-3); - } - .lg\:bui-pl-4 { - padding-left: var(--bui-space-4); - } - .lg\:bui-pl-5 { - padding-left: var(--bui-space-5); - } - .lg\:bui-pl-6 { - padding-left: var(--bui-space-6); - } - .lg\:bui-pl-7 { - padding-left: var(--bui-space-7); - } - .lg\:bui-pl-8 { - padding-left: var(--bui-space-8); - } - .lg\:bui-pl-9 { - padding-left: var(--bui-space-9); - } - .lg\:bui-pl-10 { - padding-left: var(--bui-space-10); - } - .lg\:bui-pl-11 { - padding-left: var(--bui-space-11); - } - .lg\:bui-pl-12 { - padding-left: var(--bui-space-12); - } - .lg\:bui-pl-13 { - padding-left: var(--bui-space-13); - } - .lg\:bui-pl-14 { - padding-left: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-pl { - padding-left: var(--pl-xl); - } - .xl\:bui-pl-0\.5 { - padding-left: var(--bui-space-0_5); - } - .xl\:bui-pl-1 { - padding-left: var(--bui-space-1); - } - .xl\:bui-pl-1\.5 { - padding-left: var(--bui-space-1_5); - } - .xl\:bui-pl-2 { - padding-left: var(--bui-space-2); - } - .xl\:bui-pl-3 { - padding-left: var(--bui-space-3); - } - .xl\:bui-pl-4 { - padding-left: var(--bui-space-4); - } - .xl\:bui-pl-5 { - padding-left: var(--bui-space-5); - } - .xl\:bui-pl-6 { - padding-left: var(--bui-space-6); - } - .xl\:bui-pl-7 { - padding-left: var(--bui-space-7); - } - .xl\:bui-pl-8 { - padding-left: var(--bui-space-8); - } - .xl\:bui-pl-9 { - padding-left: var(--bui-space-9); - } - .xl\:bui-pl-10 { - padding-left: var(--bui-space-10); - } - .xl\:bui-pl-11 { - padding-left: var(--bui-space-11); - } - .xl\:bui-pl-12 { - padding-left: var(--bui-space-12); - } - .xl\:bui-pl-13 { - padding-left: var(--bui-space-13); - } - .xl\:bui-pl-14 { - padding-left: var(--bui-space-14); - } - } - .bui-pr { - padding-right: var(--pr); - } - .bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .bui-pr-1 { - padding-right: var(--bui-space-1); - } - .bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .bui-pr-2 { - padding-right: var(--bui-space-2); - } - .bui-pr-3 { - padding-right: var(--bui-space-3); - } - .bui-pr-4 { - padding-right: var(--bui-space-4); - } - .bui-pr-5 { - padding-right: var(--bui-space-5); - } - .bui-pr-6 { - padding-right: var(--bui-space-6); - } - .bui-pr-7 { - padding-right: var(--bui-space-7); - } - .bui-pr-8 { - padding-right: var(--bui-space-8); - } - .bui-pr-9 { - padding-right: var(--bui-space-9); - } - .bui-pr-10 { - padding-right: var(--bui-space-10); - } - .bui-pr-11 { - padding-right: var(--bui-space-11); - } - .bui-pr-12 { - padding-right: var(--bui-space-12); - } - .bui-pr-13 { - padding-right: var(--bui-space-13); - } - .bui-pr-14 { - padding-right: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-pr { - padding-right: var(--pr-xs); - } - .xs\:bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .xs\:bui-pr-1 { - padding-right: var(--bui-space-1); - } - .xs\:bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .xs\:bui-pr-2 { - padding-right: var(--bui-space-2); - } - .xs\:bui-pr-3 { - padding-right: var(--bui-space-3); - } - .xs\:bui-pr-4 { - padding-right: var(--bui-space-4); - } - .xs\:bui-pr-5 { - padding-right: var(--bui-space-5); - } - .xs\:bui-pr-6 { - padding-right: var(--bui-space-6); - } - .xs\:bui-pr-7 { - padding-right: var(--bui-space-7); - } - .xs\:bui-pr-8 { - padding-right: var(--bui-space-8); - } - .xs\:bui-pr-9 { - padding-right: var(--bui-space-9); - } - .xs\:bui-pr-10 { - padding-right: var(--bui-space-10); - } - .xs\:bui-pr-11 { - padding-right: var(--bui-space-11); - } - .xs\:bui-pr-12 { - padding-right: var(--bui-space-12); - } - .xs\:bui-pr-13 { - padding-right: var(--bui-space-13); - } - .xs\:bui-pr-14 { - padding-right: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-pr { - padding-right: var(--pr-sm); - } - .sm\:bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .sm\:bui-pr-1 { - padding-right: var(--bui-space-1); - } - .sm\:bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .sm\:bui-pr-2 { - padding-right: var(--bui-space-2); - } - .sm\:bui-pr-3 { - padding-right: var(--bui-space-3); - } - .sm\:bui-pr-4 { - padding-right: var(--bui-space-4); - } - .sm\:bui-pr-5 { - padding-right: var(--bui-space-5); - } - .sm\:bui-pr-6 { - padding-right: var(--bui-space-6); - } - .sm\:bui-pr-7 { - padding-right: var(--bui-space-7); - } - .sm\:bui-pr-8 { - padding-right: var(--bui-space-8); - } - .sm\:bui-pr-9 { - padding-right: var(--bui-space-9); - } - .sm\:bui-pr-10 { - padding-right: var(--bui-space-10); - } - .sm\:bui-pr-11 { - padding-right: var(--bui-space-11); - } - .sm\:bui-pr-12 { - padding-right: var(--bui-space-12); - } - .sm\:bui-pr-13 { - padding-right: var(--bui-space-13); - } - .sm\:bui-pr-14 { - padding-right: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-pr { - padding-right: var(--pr-md); - } - .md\:bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .md\:bui-pr-1 { - padding-right: var(--bui-space-1); - } - .md\:bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .md\:bui-pr-2 { - padding-right: var(--bui-space-2); - } - .md\:bui-pr-3 { - padding-right: var(--bui-space-3); - } - .md\:bui-pr-4 { - padding-right: var(--bui-space-4); - } - .md\:bui-pr-5 { - padding-right: var(--bui-space-5); - } - .md\:bui-pr-6 { - padding-right: var(--bui-space-6); - } - .md\:bui-pr-7 { - padding-right: var(--bui-space-7); - } - .md\:bui-pr-8 { - padding-right: var(--bui-space-8); - } - .md\:bui-pr-9 { - padding-right: var(--bui-space-9); - } - .md\:bui-pr-10 { - padding-right: var(--bui-space-10); - } - .md\:bui-pr-11 { - padding-right: var(--bui-space-11); - } - .md\:bui-pr-12 { - padding-right: var(--bui-space-12); - } - .md\:bui-pr-13 { - padding-right: var(--bui-space-13); - } - .md\:bui-pr-14 { - padding-right: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-pr { - padding-right: var(--pr-lg); - } - .lg\:bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .lg\:bui-pr-1 { - padding-right: var(--bui-space-1); - } - .lg\:bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .lg\:bui-pr-2 { - padding-right: var(--bui-space-2); - } - .lg\:bui-pr-3 { - padding-right: var(--bui-space-3); - } - .lg\:bui-pr-4 { - padding-right: var(--bui-space-4); - } - .lg\:bui-pr-5 { - padding-right: var(--bui-space-5); - } - .lg\:bui-pr-6 { - padding-right: var(--bui-space-6); - } - .lg\:bui-pr-7 { - padding-right: var(--bui-space-7); - } - .lg\:bui-pr-8 { - padding-right: var(--bui-space-8); - } - .lg\:bui-pr-9 { - padding-right: var(--bui-space-9); - } - .lg\:bui-pr-10 { - padding-right: var(--bui-space-10); - } - .lg\:bui-pr-11 { - padding-right: var(--bui-space-11); - } - .lg\:bui-pr-12 { - padding-right: var(--bui-space-12); - } - .lg\:bui-pr-13 { - padding-right: var(--bui-space-13); - } - .lg\:bui-pr-14 { - padding-right: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-pr { - padding-right: var(--pr-xl); - } - .xl\:bui-pr-0\.5 { - padding-right: var(--bui-space-0_5); - } - .xl\:bui-pr-1 { - padding-right: var(--bui-space-1); - } - .xl\:bui-pr-1\.5 { - padding-right: var(--bui-space-1_5); - } - .xl\:bui-pr-2 { - padding-right: var(--bui-space-2); - } - .xl\:bui-pr-3 { - padding-right: var(--bui-space-3); - } - .xl\:bui-pr-4 { - padding-right: var(--bui-space-4); - } - .xl\:bui-pr-5 { - padding-right: var(--bui-space-5); - } - .xl\:bui-pr-6 { - padding-right: var(--bui-space-6); - } - .xl\:bui-pr-7 { - padding-right: var(--bui-space-7); - } - .xl\:bui-pr-8 { - padding-right: var(--bui-space-8); - } - .xl\:bui-pr-9 { - padding-right: var(--bui-space-9); - } - .xl\:bui-pr-10 { - padding-right: var(--bui-space-10); - } - .xl\:bui-pr-11 { - padding-right: var(--bui-space-11); - } - .xl\:bui-pr-12 { - padding-right: var(--bui-space-12); - } - .xl\:bui-pr-13 { - padding-right: var(--bui-space-13); - } - .xl\:bui-pr-14 { - padding-right: var(--bui-space-14); - } - } - .bui-pt { - padding-top: var(--pt); - } - .bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .bui-pt-1 { - padding-top: var(--bui-space-1); - } - .bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .bui-pt-2 { - padding-top: var(--bui-space-2); - } - .bui-pt-3 { - padding-top: var(--bui-space-3); - } - .bui-pt-4 { - padding-top: var(--bui-space-4); - } - .bui-pt-5 { - padding-top: var(--bui-space-5); - } - .bui-pt-6 { - padding-top: var(--bui-space-6); - } - .bui-pt-7 { - padding-top: var(--bui-space-7); - } - .bui-pt-8 { - padding-top: var(--bui-space-8); - } - .bui-pt-9 { - padding-top: var(--bui-space-9); - } - .bui-pt-10 { - padding-top: var(--bui-space-10); - } - .bui-pt-11 { - padding-top: var(--bui-space-11); - } - .bui-pt-12 { - padding-top: var(--bui-space-12); - } - .bui-pt-13 { - padding-top: var(--bui-space-13); - } - .bui-pt-14 { - padding-top: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-pt { - padding-top: var(--pt-xs); - } - .xs\:bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .xs\:bui-pt-1 { - padding-top: var(--bui-space-1); - } - .xs\:bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .xs\:bui-pt-2 { - padding-top: var(--bui-space-2); - } - .xs\:bui-pt-3 { - padding-top: var(--bui-space-3); - } - .xs\:bui-pt-4 { - padding-top: var(--bui-space-4); - } - .xs\:bui-pt-5 { - padding-top: var(--bui-space-5); - } - .xs\:bui-pt-6 { - padding-top: var(--bui-space-6); - } - .xs\:bui-pt-7 { - padding-top: var(--bui-space-7); - } - .xs\:bui-pt-8 { - padding-top: var(--bui-space-8); - } - .xs\:bui-pt-9 { - padding-top: var(--bui-space-9); - } - .xs\:bui-pt-10 { - padding-top: var(--bui-space-10); - } - .xs\:bui-pt-11 { - padding-top: var(--bui-space-11); - } - .xs\:bui-pt-12 { - padding-top: var(--bui-space-12); - } - .xs\:bui-pt-13 { - padding-top: var(--bui-space-13); - } - .xs\:bui-pt-14 { - padding-top: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-pt { - padding-top: var(--pt-sm); - } - .sm\:bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .sm\:bui-pt-1 { - padding-top: var(--bui-space-1); - } - .sm\:bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .sm\:bui-pt-2 { - padding-top: var(--bui-space-2); - } - .sm\:bui-pt-3 { - padding-top: var(--bui-space-3); - } - .sm\:bui-pt-4 { - padding-top: var(--bui-space-4); - } - .sm\:bui-pt-5 { - padding-top: var(--bui-space-5); - } - .sm\:bui-pt-6 { - padding-top: var(--bui-space-6); - } - .sm\:bui-pt-7 { - padding-top: var(--bui-space-7); - } - .sm\:bui-pt-8 { - padding-top: var(--bui-space-8); - } - .sm\:bui-pt-9 { - padding-top: var(--bui-space-9); - } - .sm\:bui-pt-10 { - padding-top: var(--bui-space-10); - } - .sm\:bui-pt-11 { - padding-top: var(--bui-space-11); - } - .sm\:bui-pt-12 { - padding-top: var(--bui-space-12); - } - .sm\:bui-pt-13 { - padding-top: var(--bui-space-13); - } - .sm\:bui-pt-14 { - padding-top: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-pt { - padding-top: var(--pt-md); - } - .md\:bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .md\:bui-pt-1 { - padding-top: var(--bui-space-1); - } - .md\:bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .md\:bui-pt-2 { - padding-top: var(--bui-space-2); - } - .md\:bui-pt-3 { - padding-top: var(--bui-space-3); - } - .md\:bui-pt-4 { - padding-top: var(--bui-space-4); - } - .md\:bui-pt-5 { - padding-top: var(--bui-space-5); - } - .md\:bui-pt-6 { - padding-top: var(--bui-space-6); - } - .md\:bui-pt-7 { - padding-top: var(--bui-space-7); - } - .md\:bui-pt-8 { - padding-top: var(--bui-space-8); - } - .md\:bui-pt-9 { - padding-top: var(--bui-space-9); - } - .md\:bui-pt-10 { - padding-top: var(--bui-space-10); - } - .md\:bui-pt-11 { - padding-top: var(--bui-space-11); - } - .md\:bui-pt-12 { - padding-top: var(--bui-space-12); - } - .md\:bui-pt-13 { - padding-top: var(--bui-space-13); - } - .md\:bui-pt-14 { - padding-top: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-pt { - padding-top: var(--pt-lg); - } - .lg\:bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .lg\:bui-pt-1 { - padding-top: var(--bui-space-1); - } - .lg\:bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .lg\:bui-pt-2 { - padding-top: var(--bui-space-2); - } - .lg\:bui-pt-3 { - padding-top: var(--bui-space-3); - } - .lg\:bui-pt-4 { - padding-top: var(--bui-space-4); - } - .lg\:bui-pt-5 { - padding-top: var(--bui-space-5); - } - .lg\:bui-pt-6 { - padding-top: var(--bui-space-6); - } - .lg\:bui-pt-7 { - padding-top: var(--bui-space-7); - } - .lg\:bui-pt-8 { - padding-top: var(--bui-space-8); - } - .lg\:bui-pt-9 { - padding-top: var(--bui-space-9); - } - .lg\:bui-pt-10 { - padding-top: var(--bui-space-10); - } - .lg\:bui-pt-11 { - padding-top: var(--bui-space-11); - } - .lg\:bui-pt-12 { - padding-top: var(--bui-space-12); - } - .lg\:bui-pt-13 { - padding-top: var(--bui-space-13); - } - .lg\:bui-pt-14 { - padding-top: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-pt { - padding-top: var(--pt-xl); - } - .xl\:bui-pt-0\.5 { - padding-top: var(--bui-space-0_5); - } - .xl\:bui-pt-1 { - padding-top: var(--bui-space-1); - } - .xl\:bui-pt-1\.5 { - padding-top: var(--bui-space-1_5); - } - .xl\:bui-pt-2 { - padding-top: var(--bui-space-2); - } - .xl\:bui-pt-3 { - padding-top: var(--bui-space-3); - } - .xl\:bui-pt-4 { - padding-top: var(--bui-space-4); - } - .xl\:bui-pt-5 { - padding-top: var(--bui-space-5); - } - .xl\:bui-pt-6 { - padding-top: var(--bui-space-6); - } - .xl\:bui-pt-7 { - padding-top: var(--bui-space-7); - } - .xl\:bui-pt-8 { - padding-top: var(--bui-space-8); - } - .xl\:bui-pt-9 { - padding-top: var(--bui-space-9); - } - .xl\:bui-pt-10 { - padding-top: var(--bui-space-10); - } - .xl\:bui-pt-11 { - padding-top: var(--bui-space-11); - } - .xl\:bui-pt-12 { - padding-top: var(--bui-space-12); - } - .xl\:bui-pt-13 { - padding-top: var(--bui-space-13); - } - .xl\:bui-pt-14 { - padding-top: var(--bui-space-14); - } - } - .bui-pb { - padding-bottom: var(--pb); - } - .bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-pb { - padding-bottom: var(--pb-xs); - } - .xs\:bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .xs\:bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .xs\:bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .xs\:bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .xs\:bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .xs\:bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .xs\:bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .xs\:bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .xs\:bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .xs\:bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .xs\:bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .xs\:bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .xs\:bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .xs\:bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .xs\:bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .xs\:bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-pb { - padding-bottom: var(--pb-sm); - } - .sm\:bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .sm\:bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .sm\:bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .sm\:bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .sm\:bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .sm\:bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .sm\:bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .sm\:bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .sm\:bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .sm\:bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .sm\:bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .sm\:bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .sm\:bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .sm\:bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .sm\:bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .sm\:bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-pb { - padding-bottom: var(--pb-md); - } - .md\:bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .md\:bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .md\:bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .md\:bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .md\:bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .md\:bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .md\:bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .md\:bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .md\:bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .md\:bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .md\:bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .md\:bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .md\:bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .md\:bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .md\:bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .md\:bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-pb { - padding-bottom: var(--pb-lg); - } - .lg\:bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .lg\:bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .lg\:bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .lg\:bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .lg\:bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .lg\:bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .lg\:bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .lg\:bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .lg\:bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .lg\:bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .lg\:bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .lg\:bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .lg\:bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .lg\:bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .lg\:bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .lg\:bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-pb { - padding-bottom: var(--pb-xl); - } - .xl\:bui-pb-0\.5 { - padding-bottom: var(--bui-space-0_5); - } - .xl\:bui-pb-1 { - padding-bottom: var(--bui-space-1); - } - .xl\:bui-pb-1\.5 { - padding-bottom: var(--bui-space-1_5); - } - .xl\:bui-pb-2 { - padding-bottom: var(--bui-space-2); - } - .xl\:bui-pb-3 { - padding-bottom: var(--bui-space-3); - } - .xl\:bui-pb-4 { - padding-bottom: var(--bui-space-4); - } - .xl\:bui-pb-5 { - padding-bottom: var(--bui-space-5); - } - .xl\:bui-pb-6 { - padding-bottom: var(--bui-space-6); - } - .xl\:bui-pb-7 { - padding-bottom: var(--bui-space-7); - } - .xl\:bui-pb-8 { - padding-bottom: var(--bui-space-8); - } - .xl\:bui-pb-9 { - padding-bottom: var(--bui-space-9); - } - .xl\:bui-pb-10 { - padding-bottom: var(--bui-space-10); - } - .xl\:bui-pb-11 { - padding-bottom: var(--bui-space-11); - } - .xl\:bui-pb-12 { - padding-bottom: var(--bui-space-12); - } - .xl\:bui-pb-13 { - padding-bottom: var(--bui-space-13); - } - .xl\:bui-pb-14 { - padding-bottom: var(--bui-space-14); - } - } - .bui-py { - padding-top: var(--py); - padding-bottom: var(--py); - } - .bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-py { - padding-top: var(--py-xs); - padding-bottom: var(--py-xs); - } - .xs\:bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .xs\:bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .xs\:bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .xs\:bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .xs\:bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .xs\:bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .xs\:bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .xs\:bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .xs\:bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .xs\:bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .xs\:bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .xs\:bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .xs\:bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .xs\:bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .xs\:bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .xs\:bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-py { - padding-top: var(--py-sm); - padding-bottom: var(--py-sm); - } - .sm\:bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .sm\:bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .sm\:bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .sm\:bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .sm\:bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .sm\:bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .sm\:bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .sm\:bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .sm\:bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .sm\:bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .sm\:bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .sm\:bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .sm\:bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .sm\:bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .sm\:bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .sm\:bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-py { - padding-top: var(--py-md); - padding-bottom: var(--py-md); - } - .md\:bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .md\:bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .md\:bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .md\:bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .md\:bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .md\:bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .md\:bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .md\:bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .md\:bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .md\:bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .md\:bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .md\:bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .md\:bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .md\:bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .md\:bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .md\:bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-py { - padding-top: var(--py-lg); - padding-bottom: var(--py-lg); - } - .lg\:bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .lg\:bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .lg\:bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .lg\:bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .lg\:bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .lg\:bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .lg\:bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .lg\:bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .lg\:bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .lg\:bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .lg\:bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .lg\:bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .lg\:bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .lg\:bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .lg\:bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .lg\:bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-py { - padding-top: var(--py-xl); - padding-bottom: var(--py-xl); - } - .xl\:bui-py-0\.5 { - padding-top: var(--bui-space-0_5); - padding-bottom: var(--bui-space-0_5); - } - .xl\:bui-py-1 { - padding-top: var(--bui-space-1); - padding-bottom: var(--bui-space-1); - } - .xl\:bui-py-1\.5 { - padding-top: var(--bui-space-1_5); - padding-bottom: var(--bui-space-1_5); - } - .xl\:bui-py-2 { - padding-top: var(--bui-space-2); - padding-bottom: var(--bui-space-2); - } - .xl\:bui-py-3 { - padding-top: var(--bui-space-3); - padding-bottom: var(--bui-space-3); - } - .xl\:bui-py-4 { - padding-top: var(--bui-space-4); - padding-bottom: var(--bui-space-4); - } - .xl\:bui-py-5 { - padding-top: var(--bui-space-5); - padding-bottom: var(--bui-space-5); - } - .xl\:bui-py-6 { - padding-top: var(--bui-space-6); - padding-bottom: var(--bui-space-6); - } - .xl\:bui-py-7 { - padding-top: var(--bui-space-7); - padding-bottom: var(--bui-space-7); - } - .xl\:bui-py-8 { - padding-top: var(--bui-space-8); - padding-bottom: var(--bui-space-8); - } - .xl\:bui-py-9 { - padding-top: var(--bui-space-9); - padding-bottom: var(--bui-space-9); - } - .xl\:bui-py-10 { - padding-top: var(--bui-space-10); - padding-bottom: var(--bui-space-10); - } - .xl\:bui-py-11 { - padding-top: var(--bui-space-11); - padding-bottom: var(--bui-space-11); - } - .xl\:bui-py-12 { - padding-top: var(--bui-space-12); - padding-bottom: var(--bui-space-12); - } - .xl\:bui-py-13 { - padding-top: var(--bui-space-13); - padding-bottom: var(--bui-space-13); - } - .xl\:bui-py-14 { - padding-top: var(--bui-space-14); - padding-bottom: var(--bui-space-14); - } - } - .bui-px { - padding-left: var(--px); - padding-right: var(--px); - } - .bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-px { - padding-left: var(--px-xs); - padding-right: var(--px-xs); - } - .xs\:bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .xs\:bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .xs\:bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .xs\:bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .xs\:bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .xs\:bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .xs\:bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .xs\:bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .xs\:bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .xs\:bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .xs\:bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .xs\:bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .xs\:bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .xs\:bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .xs\:bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .xs\:bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-px { - padding-left: var(--px-sm); - padding-right: var(--px-sm); - } - .sm\:bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .sm\:bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .sm\:bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .sm\:bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .sm\:bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .sm\:bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .sm\:bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .sm\:bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .sm\:bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .sm\:bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .sm\:bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .sm\:bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .sm\:bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .sm\:bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .sm\:bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .sm\:bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-px { - padding-left: var(--px-md); - padding-right: var(--px-md); - } - .md\:bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .md\:bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .md\:bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .md\:bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .md\:bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .md\:bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .md\:bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .md\:bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .md\:bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .md\:bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .md\:bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .md\:bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .md\:bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .md\:bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .md\:bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .md\:bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-px { - padding-left: var(--px-lg); - padding-right: var(--px-lg); - } - .lg\:bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .lg\:bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .lg\:bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .lg\:bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .lg\:bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .lg\:bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .lg\:bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .lg\:bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .lg\:bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .lg\:bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .lg\:bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .lg\:bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .lg\:bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .lg\:bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .lg\:bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .lg\:bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-px { - padding-left: var(--px-xl); - padding-right: var(--px-xl); - } - .xl\:bui-px-0\.5 { - padding-left: var(--bui-space-0_5); - padding-right: var(--bui-space-0_5); - } - .xl\:bui-px-1 { - padding-left: var(--bui-space-1); - padding-right: var(--bui-space-1); - } - .xl\:bui-px-1\.5 { - padding-left: var(--bui-space-1_5); - padding-right: var(--bui-space-1_5); - } - .xl\:bui-px-2 { - padding-left: var(--bui-space-2); - padding-right: var(--bui-space-2); - } - .xl\:bui-px-3 { - padding-left: var(--bui-space-3); - padding-right: var(--bui-space-3); - } - .xl\:bui-px-4 { - padding-left: var(--bui-space-4); - padding-right: var(--bui-space-4); - } - .xl\:bui-px-5 { - padding-left: var(--bui-space-5); - padding-right: var(--bui-space-5); - } - .xl\:bui-px-6 { - padding-left: var(--bui-space-6); - padding-right: var(--bui-space-6); - } - .xl\:bui-px-7 { - padding-left: var(--bui-space-7); - padding-right: var(--bui-space-7); - } - .xl\:bui-px-8 { - padding-left: var(--bui-space-8); - padding-right: var(--bui-space-8); - } - .xl\:bui-px-9 { - padding-left: var(--bui-space-9); - padding-right: var(--bui-space-9); - } - .xl\:bui-px-10 { - padding-left: var(--bui-space-10); - padding-right: var(--bui-space-10); - } - .xl\:bui-px-11 { - padding-left: var(--bui-space-11); - padding-right: var(--bui-space-11); - } - .xl\:bui-px-12 { - padding-left: var(--bui-space-12); - padding-right: var(--bui-space-12); - } - .xl\:bui-px-13 { - padding-left: var(--bui-space-13); - padding-right: var(--bui-space-13); - } - .xl\:bui-px-14 { - padding-left: var(--bui-space-14); - padding-right: var(--bui-space-14); - } - } - .bui-m { - margin: var(--m); - } - .bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .bui-m-1 { - margin: var(--bui-space-1); - } - .bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .bui-m-2 { - margin: var(--bui-space-2); - } - .bui-m-3 { - margin: var(--bui-space-3); - } - .bui-m-4 { - margin: var(--bui-space-4); - } - .bui-m-5 { - margin: var(--bui-space-5); - } - .bui-m-6 { - margin: var(--bui-space-6); - } - .bui-m-7 { - margin: var(--bui-space-7); - } - .bui-m-8 { - margin: var(--bui-space-8); - } - .bui-m-9 { - margin: var(--bui-space-9); - } - .bui-m-10 { - margin: var(--bui-space-10); - } - .bui-m-11 { - margin: var(--bui-space-11); - } - .bui-m-12 { - margin: var(--bui-space-12); - } - .bui-m-13 { - margin: var(--bui-space-13); - } - .bui-m-14 { - margin: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-m { - margin: var(--p-xs); - } - .xs\:bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .xs\:bui-m-1 { - margin: var(--bui-space-1); - } - .xs\:bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .xs\:bui-m-2 { - margin: var(--bui-space-2); - } - .xs\:bui-m-3 { - margin: var(--bui-space-3); - } - .xs\:bui-m-4 { - margin: var(--bui-space-4); - } - .xs\:bui-m-5 { - margin: var(--bui-space-5); - } - .xs\:bui-m-6 { - margin: var(--bui-space-6); - } - .xs\:bui-m-7 { - margin: var(--bui-space-7); - } - .xs\:bui-m-8 { - margin: var(--bui-space-8); - } - .xs\:bui-m-9 { - margin: var(--bui-space-9); - } - .xs\:bui-m-10 { - margin: var(--bui-space-10); - } - .xs\:bui-m-11 { - margin: var(--bui-space-11); - } - .xs\:bui-m-12 { - margin: var(--bui-space-12); - } - .xs\:bui-m-13 { - margin: var(--bui-space-13); - } - .xs\:bui-m-14 { - margin: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-m { - margin: var(--p-sm); - } - .sm\:bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .sm\:bui-m-1 { - margin: var(--bui-space-1); - } - .sm\:bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .sm\:bui-m-2 { - margin: var(--bui-space-2); - } - .sm\:bui-m-3 { - margin: var(--bui-space-3); - } - .sm\:bui-m-4 { - margin: var(--bui-space-4); - } - .sm\:bui-m-5 { - margin: var(--bui-space-5); - } - .sm\:bui-m-6 { - margin: var(--bui-space-6); - } - .sm\:bui-m-7 { - margin: var(--bui-space-7); - } - .sm\:bui-m-8 { - margin: var(--bui-space-8); - } - .sm\:bui-m-9 { - margin: var(--bui-space-9); - } - .sm\:bui-m-10 { - margin: var(--bui-space-10); - } - .sm\:bui-m-11 { - margin: var(--bui-space-11); - } - .sm\:bui-m-12 { - margin: var(--bui-space-12); - } - .sm\:bui-m-13 { - margin: var(--bui-space-13); - } - .sm\:bui-m-14 { - margin: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-m { - margin: var(--p-md); - } - .md\:bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .md\:bui-m-1 { - margin: var(--bui-space-1); - } - .md\:bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .md\:bui-m-2 { - margin: var(--bui-space-2); - } - .md\:bui-m-3 { - margin: var(--bui-space-3); - } - .md\:bui-m-4 { - margin: var(--bui-space-4); - } - .md\:bui-m-5 { - margin: var(--bui-space-5); - } - .md\:bui-m-6 { - margin: var(--bui-space-6); - } - .md\:bui-m-7 { - margin: var(--bui-space-7); - } - .md\:bui-m-8 { - margin: var(--bui-space-8); - } - .md\:bui-m-9 { - margin: var(--bui-space-9); - } - .md\:bui-m-10 { - margin: var(--bui-space-10); - } - .md\:bui-m-11 { - margin: var(--bui-space-11); - } - .md\:bui-m-12 { - margin: var(--bui-space-12); - } - .md\:bui-m-13 { - margin: var(--bui-space-13); - } - .md\:bui-m-14 { - margin: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-m { - margin: var(--p-lg); - } - .lg\:bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .lg\:bui-m-1 { - margin: var(--bui-space-1); - } - .lg\:bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .lg\:bui-m-2 { - margin: var(--bui-space-2); - } - .lg\:bui-m-3 { - margin: var(--bui-space-3); - } - .lg\:bui-m-4 { - margin: var(--bui-space-4); - } - .lg\:bui-m-5 { - margin: var(--bui-space-5); - } - .lg\:bui-m-6 { - margin: var(--bui-space-6); - } - .lg\:bui-m-7 { - margin: var(--bui-space-7); - } - .lg\:bui-m-8 { - margin: var(--bui-space-8); - } - .lg\:bui-m-9 { - margin: var(--bui-space-9); - } - .lg\:bui-m-10 { - margin: var(--bui-space-10); - } - .lg\:bui-m-11 { - margin: var(--bui-space-11); - } - .lg\:bui-m-12 { - margin: var(--bui-space-12); - } - .lg\:bui-m-13 { - margin: var(--bui-space-13); - } - .lg\:bui-m-14 { - margin: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-m { - margin: var(--p-xl); - } - .xl\:bui-m-0\.5 { - margin: var(--bui-space-0_5); - } - .xl\:bui-m-1 { - margin: var(--bui-space-1); - } - .xl\:bui-m-1\.5 { - margin: var(--bui-space-1_5); - } - .xl\:bui-m-2 { - margin: var(--bui-space-2); - } - .xl\:bui-m-3 { - margin: var(--bui-space-3); - } - .xl\:bui-m-4 { - margin: var(--bui-space-4); - } - .xl\:bui-m-5 { - margin: var(--bui-space-5); - } - .xl\:bui-m-6 { - margin: var(--bui-space-6); - } - .xl\:bui-m-7 { - margin: var(--bui-space-7); - } - .xl\:bui-m-8 { - margin: var(--bui-space-8); - } - .xl\:bui-m-9 { - margin: var(--bui-space-9); - } - .xl\:bui-m-10 { - margin: var(--bui-space-10); - } - .xl\:bui-m-11 { - margin: var(--bui-space-11); - } - .xl\:bui-m-12 { - margin: var(--bui-space-12); - } - .xl\:bui-m-13 { - margin: var(--bui-space-13); - } - .xl\:bui-m-14 { - margin: var(--bui-space-14); - } - } - .bui-ml { - margin-left: var(--ml); - } - .bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .bui-ml-1 { - margin-left: var(--bui-space-1); - } - .bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .bui-ml-2 { - margin-left: var(--bui-space-2); - } - .bui-ml-3 { - margin-left: var(--bui-space-3); - } - .bui-ml-4 { - margin-left: var(--bui-space-4); - } - .bui-ml-5 { - margin-left: var(--bui-space-5); - } - .bui-ml-6 { - margin-left: var(--bui-space-6); - } - .bui-ml-7 { - margin-left: var(--bui-space-7); - } - .bui-ml-8 { - margin-left: var(--bui-space-8); - } - .bui-ml-9 { - margin-left: var(--bui-space-9); - } - .bui-ml-10 { - margin-left: var(--bui-space-10); - } - .bui-ml-11 { - margin-left: var(--bui-space-11); - } - .bui-ml-12 { - margin-left: var(--bui-space-12); - } - .bui-ml-13 { - margin-left: var(--bui-space-13); - } - .bui-ml-14 { - margin-left: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-ml { - margin-left: var(--ml-xs); - } - .xs\:bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .xs\:bui-ml-1 { - margin-left: var(--bui-space-1); - } - .xs\:bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .xs\:bui-ml-2 { - margin-left: var(--bui-space-2); - } - .xs\:bui-ml-3 { - margin-left: var(--bui-space-3); - } - .xs\:bui-ml-4 { - margin-left: var(--bui-space-4); - } - .xs\:bui-ml-5 { - margin-left: var(--bui-space-5); - } - .xs\:bui-ml-6 { - margin-left: var(--bui-space-6); - } - .xs\:bui-ml-7 { - margin-left: var(--bui-space-7); - } - .xs\:bui-ml-8 { - margin-left: var(--bui-space-8); - } - .xs\:bui-ml-9 { - margin-left: var(--bui-space-9); - } - .xs\:bui-ml-10 { - margin-left: var(--bui-space-10); - } - .xs\:bui-ml-11 { - margin-left: var(--bui-space-11); - } - .xs\:bui-ml-12 { - margin-left: var(--bui-space-12); - } - .xs\:bui-ml-13 { - margin-left: var(--bui-space-13); - } - .xs\:bui-ml-14 { - margin-left: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-ml { - margin-left: var(--ml-sm); - } - .sm\:bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .sm\:bui-ml-1 { - margin-left: var(--bui-space-1); - } - .sm\:bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .sm\:bui-ml-2 { - margin-left: var(--bui-space-2); - } - .sm\:bui-ml-3 { - margin-left: var(--bui-space-3); - } - .sm\:bui-ml-4 { - margin-left: var(--bui-space-4); - } - .sm\:bui-ml-5 { - margin-left: var(--bui-space-5); - } - .sm\:bui-ml-6 { - margin-left: var(--bui-space-6); - } - .sm\:bui-ml-7 { - margin-left: var(--bui-space-7); - } - .sm\:bui-ml-8 { - margin-left: var(--bui-space-8); - } - .sm\:bui-ml-9 { - margin-left: var(--bui-space-9); - } - .sm\:bui-ml-10 { - margin-left: var(--bui-space-10); - } - .sm\:bui-ml-11 { - margin-left: var(--bui-space-11); - } - .sm\:bui-ml-12 { - margin-left: var(--bui-space-12); - } - .sm\:bui-ml-13 { - margin-left: var(--bui-space-13); - } - .sm\:bui-ml-14 { - margin-left: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-ml { - margin-left: var(--ml-md); - } - .md\:bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .md\:bui-ml-1 { - margin-left: var(--bui-space-1); - } - .md\:bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .md\:bui-ml-2 { - margin-left: var(--bui-space-2); - } - .md\:bui-ml-3 { - margin-left: var(--bui-space-3); - } - .md\:bui-ml-4 { - margin-left: var(--bui-space-4); - } - .md\:bui-ml-5 { - margin-left: var(--bui-space-5); - } - .md\:bui-ml-6 { - margin-left: var(--bui-space-6); - } - .md\:bui-ml-7 { - margin-left: var(--bui-space-7); - } - .md\:bui-ml-8 { - margin-left: var(--bui-space-8); - } - .md\:bui-ml-9 { - margin-left: var(--bui-space-9); - } - .md\:bui-ml-10 { - margin-left: var(--bui-space-10); - } - .md\:bui-ml-11 { - margin-left: var(--bui-space-11); - } - .md\:bui-ml-12 { - margin-left: var(--bui-space-12); - } - .md\:bui-ml-13 { - margin-left: var(--bui-space-13); - } - .md\:bui-ml-14 { - margin-left: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-ml { - margin-left: var(--ml-lg); - } - .lg\:bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .lg\:bui-ml-1 { - margin-left: var(--bui-space-1); - } - .lg\:bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .lg\:bui-ml-2 { - margin-left: var(--bui-space-2); - } - .lg\:bui-ml-3 { - margin-left: var(--bui-space-3); - } - .lg\:bui-ml-4 { - margin-left: var(--bui-space-4); - } - .lg\:bui-ml-5 { - margin-left: var(--bui-space-5); - } - .lg\:bui-ml-6 { - margin-left: var(--bui-space-6); - } - .lg\:bui-ml-7 { - margin-left: var(--bui-space-7); - } - .lg\:bui-ml-8 { - margin-left: var(--bui-space-8); - } - .lg\:bui-ml-9 { - margin-left: var(--bui-space-9); - } - .lg\:bui-ml-10 { - margin-left: var(--bui-space-10); - } - .lg\:bui-ml-11 { - margin-left: var(--bui-space-11); - } - .lg\:bui-ml-12 { - margin-left: var(--bui-space-12); - } - .lg\:bui-ml-13 { - margin-left: var(--bui-space-13); - } - .lg\:bui-ml-14 { - margin-left: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-ml { - margin-left: var(--ml-xl); - } - .xl\:bui-ml-0\.5 { - margin-left: var(--bui-space-0_5); - } - .xl\:bui-ml-1 { - margin-left: var(--bui-space-1); - } - .xl\:bui-ml-1\.5 { - margin-left: var(--bui-space-1_5); - } - .xl\:bui-ml-2 { - margin-left: var(--bui-space-2); - } - .xl\:bui-ml-3 { - margin-left: var(--bui-space-3); - } - .xl\:bui-ml-4 { - margin-left: var(--bui-space-4); - } - .xl\:bui-ml-5 { - margin-left: var(--bui-space-5); - } - .xl\:bui-ml-6 { - margin-left: var(--bui-space-6); - } - .xl\:bui-ml-7 { - margin-left: var(--bui-space-7); - } - .xl\:bui-ml-8 { - margin-left: var(--bui-space-8); - } - .xl\:bui-ml-9 { - margin-left: var(--bui-space-9); - } - .xl\:bui-ml-10 { - margin-left: var(--bui-space-10); - } - .xl\:bui-ml-11 { - margin-left: var(--bui-space-11); - } - .xl\:bui-ml-12 { - margin-left: var(--bui-space-12); - } - .xl\:bui-ml-13 { - margin-left: var(--bui-space-13); - } - .xl\:bui-ml-14 { - margin-left: var(--bui-space-14); - } - } - .bui-mr { - margin-right: var(--mr); - } - .bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .bui-mr-1 { - margin-right: var(--bui-space-1); - } - .bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .bui-mr-2 { - margin-right: var(--bui-space-2); - } - .bui-mr-3 { - margin-right: var(--bui-space-3); - } - .bui-mr-4 { - margin-right: var(--bui-space-4); - } - .bui-mr-5 { - margin-right: var(--bui-space-5); - } - .bui-mr-6 { - margin-right: var(--bui-space-6); - } - .bui-mr-7 { - margin-right: var(--bui-space-7); - } - .bui-mr-8 { - margin-right: var(--bui-space-8); - } - .bui-mr-9 { - margin-right: var(--bui-space-9); - } - .bui-mr-10 { - margin-right: var(--bui-space-10); - } - .bui-mr-11 { - margin-right: var(--bui-space-11); - } - .bui-mr-12 { - margin-right: var(--bui-space-12); - } - .bui-mr-13 { - margin-right: var(--bui-space-13); - } - .bui-mr-14 { - margin-right: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-mr { - margin-right: var(--mr-xs); - } - .xs\:bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .xs\:bui-mr-1 { - margin-right: var(--bui-space-1); - } - .xs\:bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .xs\:bui-mr-2 { - margin-right: var(--bui-space-2); - } - .xs\:bui-mr-3 { - margin-right: var(--bui-space-3); - } - .xs\:bui-mr-4 { - margin-right: var(--bui-space-4); - } - .xs\:bui-mr-5 { - margin-right: var(--bui-space-5); - } - .xs\:bui-mr-6 { - margin-right: var(--bui-space-6); - } - .xs\:bui-mr-7 { - margin-right: var(--bui-space-7); - } - .xs\:bui-mr-8 { - margin-right: var(--bui-space-8); - } - .xs\:bui-mr-9 { - margin-right: var(--bui-space-9); - } - .xs\:bui-mr-10 { - margin-right: var(--bui-space-10); - } - .xs\:bui-mr-11 { - margin-right: var(--bui-space-11); - } - .xs\:bui-mr-12 { - margin-right: var(--bui-space-12); - } - .xs\:bui-mr-13 { - margin-right: var(--bui-space-13); - } - .xs\:bui-mr-14 { - margin-right: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-mr { - margin-right: var(--mr-sm); - } - .sm\:bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .sm\:bui-mr-1 { - margin-right: var(--bui-space-1); - } - .sm\:bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .sm\:bui-mr-2 { - margin-right: var(--bui-space-2); - } - .sm\:bui-mr-3 { - margin-right: var(--bui-space-3); - } - .sm\:bui-mr-4 { - margin-right: var(--bui-space-4); - } - .sm\:bui-mr-5 { - margin-right: var(--bui-space-5); - } - .sm\:bui-mr-6 { - margin-right: var(--bui-space-6); - } - .sm\:bui-mr-7 { - margin-right: var(--bui-space-7); - } - .sm\:bui-mr-8 { - margin-right: var(--bui-space-8); - } - .sm\:bui-mr-9 { - margin-right: var(--bui-space-9); - } - .sm\:bui-mr-10 { - margin-right: var(--bui-space-10); - } - .sm\:bui-mr-11 { - margin-right: var(--bui-space-11); - } - .sm\:bui-mr-12 { - margin-right: var(--bui-space-12); - } - .sm\:bui-mr-13 { - margin-right: var(--bui-space-13); - } - .sm\:bui-mr-14 { - margin-right: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-mr { - margin-right: var(--mr-md); - } - .md\:bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .md\:bui-mr-1 { - margin-right: var(--bui-space-1); - } - .md\:bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .md\:bui-mr-2 { - margin-right: var(--bui-space-2); - } - .md\:bui-mr-3 { - margin-right: var(--bui-space-3); - } - .md\:bui-mr-4 { - margin-right: var(--bui-space-4); - } - .md\:bui-mr-5 { - margin-right: var(--bui-space-5); - } - .md\:bui-mr-6 { - margin-right: var(--bui-space-6); - } - .md\:bui-mr-7 { - margin-right: var(--bui-space-7); - } - .md\:bui-mr-8 { - margin-right: var(--bui-space-8); - } - .md\:bui-mr-9 { - margin-right: var(--bui-space-9); - } - .md\:bui-mr-10 { - margin-right: var(--bui-space-10); - } - .md\:bui-mr-11 { - margin-right: var(--bui-space-11); - } - .md\:bui-mr-12 { - margin-right: var(--bui-space-12); - } - .md\:bui-mr-13 { - margin-right: var(--bui-space-13); - } - .md\:bui-mr-14 { - margin-right: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-mr { - margin-right: var(--mr-lg); - } - .lg\:bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .lg\:bui-mr-1 { - margin-right: var(--bui-space-1); - } - .lg\:bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .lg\:bui-mr-2 { - margin-right: var(--bui-space-2); - } - .lg\:bui-mr-3 { - margin-right: var(--bui-space-3); - } - .lg\:bui-mr-4 { - margin-right: var(--bui-space-4); - } - .lg\:bui-mr-5 { - margin-right: var(--bui-space-5); - } - .lg\:bui-mr-6 { - margin-right: var(--bui-space-6); - } - .lg\:bui-mr-7 { - margin-right: var(--bui-space-7); - } - .lg\:bui-mr-8 { - margin-right: var(--bui-space-8); - } - .lg\:bui-mr-9 { - margin-right: var(--bui-space-9); - } - .lg\:bui-mr-10 { - margin-right: var(--bui-space-10); - } - .lg\:bui-mr-11 { - margin-right: var(--bui-space-11); - } - .lg\:bui-mr-12 { - margin-right: var(--bui-space-12); - } - .lg\:bui-mr-13 { - margin-right: var(--bui-space-13); - } - .lg\:bui-mr-14 { - margin-right: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-mr { - margin-right: var(--mr-xl); - } - .xl\:bui-mr-0\.5 { - margin-right: var(--bui-space-0_5); - } - .xl\:bui-mr-1 { - margin-right: var(--bui-space-1); - } - .xl\:bui-mr-1\.5 { - margin-right: var(--bui-space-1_5); - } - .xl\:bui-mr-2 { - margin-right: var(--bui-space-2); - } - .xl\:bui-mr-3 { - margin-right: var(--bui-space-3); - } - .xl\:bui-mr-4 { - margin-right: var(--bui-space-4); - } - .xl\:bui-mr-5 { - margin-right: var(--bui-space-5); - } - .xl\:bui-mr-6 { - margin-right: var(--bui-space-6); - } - .xl\:bui-mr-7 { - margin-right: var(--bui-space-7); - } - .xl\:bui-mr-8 { - margin-right: var(--bui-space-8); - } - .xl\:bui-mr-9 { - margin-right: var(--bui-space-9); - } - .xl\:bui-mr-10 { - margin-right: var(--bui-space-10); - } - .xl\:bui-mr-11 { - margin-right: var(--bui-space-11); - } - .xl\:bui-mr-12 { - margin-right: var(--bui-space-12); - } - .xl\:bui-mr-13 { - margin-right: var(--bui-space-13); - } - .xl\:bui-mr-14 { - margin-right: var(--bui-space-14); - } - } - .bui-mt { - margin-top: var(--mt); - } - .bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .bui-mt-1 { - margin-top: var(--bui-space-1); - } - .bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .bui-mt-2 { - margin-top: var(--bui-space-2); - } - .bui-mt-3 { - margin-top: var(--bui-space-3); - } - .bui-mt-4 { - margin-top: var(--bui-space-4); - } - .bui-mt-5 { - margin-top: var(--bui-space-5); - } - .bui-mt-6 { - margin-top: var(--bui-space-6); - } - .bui-mt-7 { - margin-top: var(--bui-space-7); - } - .bui-mt-8 { - margin-top: var(--bui-space-8); - } - .bui-mt-9 { - margin-top: var(--bui-space-9); - } - .bui-mt-10 { - margin-top: var(--bui-space-10); - } - .bui-mt-11 { - margin-top: var(--bui-space-11); - } - .bui-mt-12 { - margin-top: var(--bui-space-12); - } - .bui-mt-13 { - margin-top: var(--bui-space-13); - } - .bui-mt-14 { - margin-top: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-mt { - margin-top: var(--mt-xs); - } - .xs\:bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .xs\:bui-mt-1 { - margin-top: var(--bui-space-1); - } - .xs\:bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .xs\:bui-mt-2 { - margin-top: var(--bui-space-2); - } - .xs\:bui-mt-3 { - margin-top: var(--bui-space-3); - } - .xs\:bui-mt-4 { - margin-top: var(--bui-space-4); - } - .xs\:bui-mt-5 { - margin-top: var(--bui-space-5); - } - .xs\:bui-mt-6 { - margin-top: var(--bui-space-6); - } - .xs\:bui-mt-7 { - margin-top: var(--bui-space-7); - } - .xs\:bui-mt-8 { - margin-top: var(--bui-space-8); - } - .xs\:bui-mt-9 { - margin-top: var(--bui-space-9); - } - .xs\:bui-mt-10 { - margin-top: var(--bui-space-10); - } - .xs\:bui-mt-11 { - margin-top: var(--bui-space-11); - } - .xs\:bui-mt-12 { - margin-top: var(--bui-space-12); - } - .xs\:bui-mt-13 { - margin-top: var(--bui-space-13); - } - .xs\:bui-mt-14 { - margin-top: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-mt { - margin-top: var(--mt-sm); - } - .sm\:bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .sm\:bui-mt-1 { - margin-top: var(--bui-space-1); - } - .sm\:bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .sm\:bui-mt-2 { - margin-top: var(--bui-space-2); - } - .sm\:bui-mt-3 { - margin-top: var(--bui-space-3); - } - .sm\:bui-mt-4 { - margin-top: var(--bui-space-4); - } - .sm\:bui-mt-5 { - margin-top: var(--bui-space-5); - } - .sm\:bui-mt-6 { - margin-top: var(--bui-space-6); - } - .sm\:bui-mt-7 { - margin-top: var(--bui-space-7); - } - .sm\:bui-mt-8 { - margin-top: var(--bui-space-8); - } - .sm\:bui-mt-9 { - margin-top: var(--bui-space-9); - } - .sm\:bui-mt-10 { - margin-top: var(--bui-space-10); - } - .sm\:bui-mt-11 { - margin-top: var(--bui-space-11); - } - .sm\:bui-mt-12 { - margin-top: var(--bui-space-12); - } - .sm\:bui-mt-13 { - margin-top: var(--bui-space-13); - } - .sm\:bui-mt-14 { - margin-top: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-mt { - margin-top: var(--mt-md); - } - .md\:bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .md\:bui-mt-1 { - margin-top: var(--bui-space-1); - } - .md\:bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .md\:bui-mt-2 { - margin-top: var(--bui-space-2); - } - .md\:bui-mt-3 { - margin-top: var(--bui-space-3); - } - .md\:bui-mt-4 { - margin-top: var(--bui-space-4); - } - .md\:bui-mt-5 { - margin-top: var(--bui-space-5); - } - .md\:bui-mt-6 { - margin-top: var(--bui-space-6); - } - .md\:bui-mt-7 { - margin-top: var(--bui-space-7); - } - .md\:bui-mt-8 { - margin-top: var(--bui-space-8); - } - .md\:bui-mt-9 { - margin-top: var(--bui-space-9); - } - .md\:bui-mt-10 { - margin-top: var(--bui-space-10); - } - .md\:bui-mt-11 { - margin-top: var(--bui-space-11); - } - .md\:bui-mt-12 { - margin-top: var(--bui-space-12); - } - .md\:bui-mt-13 { - margin-top: var(--bui-space-13); - } - .md\:bui-mt-14 { - margin-top: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-mt { - margin-top: var(--mt-lg); - } - .lg\:bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .lg\:bui-mt-1 { - margin-top: var(--bui-space-1); - } - .lg\:bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .lg\:bui-mt-2 { - margin-top: var(--bui-space-2); - } - .lg\:bui-mt-3 { - margin-top: var(--bui-space-3); - } - .lg\:bui-mt-4 { - margin-top: var(--bui-space-4); - } - .lg\:bui-mt-5 { - margin-top: var(--bui-space-5); - } - .lg\:bui-mt-6 { - margin-top: var(--bui-space-6); - } - .lg\:bui-mt-7 { - margin-top: var(--bui-space-7); - } - .lg\:bui-mt-8 { - margin-top: var(--bui-space-8); - } - .lg\:bui-mt-9 { - margin-top: var(--bui-space-9); - } - .lg\:bui-mt-10 { - margin-top: var(--bui-space-10); - } - .lg\:bui-mt-11 { - margin-top: var(--bui-space-11); - } - .lg\:bui-mt-12 { - margin-top: var(--bui-space-12); - } - .lg\:bui-mt-13 { - margin-top: var(--bui-space-13); - } - .lg\:bui-mt-14 { - margin-top: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-mt { - margin-top: var(--mt-xl); - } - .xl\:bui-mt-0\.5 { - margin-top: var(--bui-space-0_5); - } - .xl\:bui-mt-1 { - margin-top: var(--bui-space-1); - } - .xl\:bui-mt-1\.5 { - margin-top: var(--bui-space-1_5); - } - .xl\:bui-mt-2 { - margin-top: var(--bui-space-2); - } - .xl\:bui-mt-3 { - margin-top: var(--bui-space-3); - } - .xl\:bui-mt-4 { - margin-top: var(--bui-space-4); - } - .xl\:bui-mt-5 { - margin-top: var(--bui-space-5); - } - .xl\:bui-mt-6 { - margin-top: var(--bui-space-6); - } - .xl\:bui-mt-7 { - margin-top: var(--bui-space-7); - } - .xl\:bui-mt-8 { - margin-top: var(--bui-space-8); - } - .xl\:bui-mt-9 { - margin-top: var(--bui-space-9); - } - .xl\:bui-mt-10 { - margin-top: var(--bui-space-10); - } - .xl\:bui-mt-11 { - margin-top: var(--bui-space-11); - } - .xl\:bui-mt-12 { - margin-top: var(--bui-space-12); - } - .xl\:bui-mt-13 { - margin-top: var(--bui-space-13); - } - .xl\:bui-mt-14 { - margin-top: var(--bui-space-14); - } - } - .bui-mb { - margin-bottom: var(--mb); - } - .bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-mb { - margin-bottom: var(--mb-xs); - } - .xs\:bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .xs\:bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .xs\:bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .xs\:bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .xs\:bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .xs\:bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .xs\:bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .xs\:bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .xs\:bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .xs\:bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .xs\:bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .xs\:bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .xs\:bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .xs\:bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .xs\:bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .xs\:bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-mb { - margin-bottom: var(--mb-sm); - } - .sm\:bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .sm\:bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .sm\:bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .sm\:bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .sm\:bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .sm\:bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .sm\:bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .sm\:bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .sm\:bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .sm\:bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .sm\:bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .sm\:bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .sm\:bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .sm\:bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .sm\:bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .sm\:bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-mb { - margin-bottom: var(--mb-md); - } - .md\:bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .md\:bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .md\:bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .md\:bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .md\:bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .md\:bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .md\:bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .md\:bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .md\:bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .md\:bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .md\:bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .md\:bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .md\:bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .md\:bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .md\:bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .md\:bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-mb { - margin-bottom: var(--mb-lg); - } - .lg\:bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .lg\:bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .lg\:bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .lg\:bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .lg\:bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .lg\:bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .lg\:bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .lg\:bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .lg\:bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .lg\:bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .lg\:bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .lg\:bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .lg\:bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .lg\:bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .lg\:bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .lg\:bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-mb { - margin-bottom: var(--mb-xl); - } - .xl\:bui-mb-0\.5 { - margin-bottom: var(--bui-space-0_5); - } - .xl\:bui-mb-1 { - margin-bottom: var(--bui-space-1); - } - .xl\:bui-mb-1\.5 { - margin-bottom: var(--bui-space-1_5); - } - .xl\:bui-mb-2 { - margin-bottom: var(--bui-space-2); - } - .xl\:bui-mb-3 { - margin-bottom: var(--bui-space-3); - } - .xl\:bui-mb-4 { - margin-bottom: var(--bui-space-4); - } - .xl\:bui-mb-5 { - margin-bottom: var(--bui-space-5); - } - .xl\:bui-mb-6 { - margin-bottom: var(--bui-space-6); - } - .xl\:bui-mb-7 { - margin-bottom: var(--bui-space-7); - } - .xl\:bui-mb-8 { - margin-bottom: var(--bui-space-8); - } - .xl\:bui-mb-9 { - margin-bottom: var(--bui-space-9); - } - .xl\:bui-mb-10 { - margin-bottom: var(--bui-space-10); - } - .xl\:bui-mb-11 { - margin-bottom: var(--bui-space-11); - } - .xl\:bui-mb-12 { - margin-bottom: var(--bui-space-12); - } - .xl\:bui-mb-13 { - margin-bottom: var(--bui-space-13); - } - .xl\:bui-mb-14 { - margin-bottom: var(--bui-space-14); - } - } - .bui-my { - margin-top: var(--my); - margin-bottom: var(--my); - } - .bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-my { - margin-top: var(--my-xs); - margin-bottom: var(--my-xs); - } - .xs\:bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .xs\:bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .xs\:bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .xs\:bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .xs\:bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .xs\:bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .xs\:bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .xs\:bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .xs\:bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .xs\:bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .xs\:bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .xs\:bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .xs\:bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .xs\:bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .xs\:bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .xs\:bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-my { - margin-top: var(--my-sm); - margin-bottom: var(--my-sm); - } - .sm\:bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .sm\:bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .sm\:bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .sm\:bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .sm\:bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .sm\:bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .sm\:bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .sm\:bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .sm\:bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .sm\:bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .sm\:bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .sm\:bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .sm\:bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .sm\:bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .sm\:bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .sm\:bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-my { - margin-top: var(--my-md); - margin-bottom: var(--my-md); - } - .md\:bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .md\:bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .md\:bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .md\:bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .md\:bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .md\:bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .md\:bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .md\:bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .md\:bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .md\:bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .md\:bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .md\:bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .md\:bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .md\:bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .md\:bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .md\:bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-my { - margin-top: var(--my-lg); - margin-bottom: var(--my-lg); - } - .lg\:bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .lg\:bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .lg\:bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .lg\:bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .lg\:bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .lg\:bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .lg\:bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .lg\:bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .lg\:bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .lg\:bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .lg\:bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .lg\:bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .lg\:bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .lg\:bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .lg\:bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .lg\:bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-my { - margin-top: var(--my-xl); - margin-bottom: var(--my-xl); - } - .xl\:bui-my-0\.5 { - margin-top: var(--bui-space-0_5); - margin-bottom: var(--bui-space-0_5); - } - .xl\:bui-my-1 { - margin-top: var(--bui-space-1); - margin-bottom: var(--bui-space-1); - } - .xl\:bui-my-1\.5 { - margin-top: var(--bui-space-1_5); - margin-bottom: var(--bui-space-1_5); - } - .xl\:bui-my-2 { - margin-top: var(--bui-space-2); - margin-bottom: var(--bui-space-2); - } - .xl\:bui-my-3 { - margin-top: var(--bui-space-3); - margin-bottom: var(--bui-space-3); - } - .xl\:bui-my-4 { - margin-top: var(--bui-space-4); - margin-bottom: var(--bui-space-4); - } - .xl\:bui-my-5 { - margin-top: var(--bui-space-5); - margin-bottom: var(--bui-space-5); - } - .xl\:bui-my-6 { - margin-top: var(--bui-space-6); - margin-bottom: var(--bui-space-6); - } - .xl\:bui-my-7 { - margin-top: var(--bui-space-7); - margin-bottom: var(--bui-space-7); - } - .xl\:bui-my-8 { - margin-top: var(--bui-space-8); - margin-bottom: var(--bui-space-8); - } - .xl\:bui-my-9 { - margin-top: var(--bui-space-9); - margin-bottom: var(--bui-space-9); - } - .xl\:bui-my-10 { - margin-top: var(--bui-space-10); - margin-bottom: var(--bui-space-10); - } - .xl\:bui-my-11 { - margin-top: var(--bui-space-11); - margin-bottom: var(--bui-space-11); - } - .xl\:bui-my-12 { - margin-top: var(--bui-space-12); - margin-bottom: var(--bui-space-12); - } - .xl\:bui-my-13 { - margin-top: var(--bui-space-13); - margin-bottom: var(--bui-space-13); - } - .xl\:bui-my-14 { - margin-top: var(--bui-space-14); - margin-bottom: var(--bui-space-14); - } - } - .bui-mx { - margin-left: var(--mx); - margin-right: var(--mx); - } - .bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-mx { - margin-left: var(--mx-xs); - margin-right: var(--mx-xs); - } - .xs\:bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .xs\:bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .xs\:bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .xs\:bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .xs\:bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .xs\:bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .xs\:bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .xs\:bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .xs\:bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .xs\:bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .xs\:bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .xs\:bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .xs\:bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .xs\:bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .xs\:bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .xs\:bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-mx { - margin-left: var(--mx-sm); - margin-right: var(--mx-sm); - } - .sm\:bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .sm\:bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .sm\:bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .sm\:bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .sm\:bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .sm\:bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .sm\:bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .sm\:bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .sm\:bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .sm\:bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .sm\:bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .sm\:bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .sm\:bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .sm\:bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .sm\:bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .sm\:bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-mx { - margin-left: var(--mx-md); - margin-right: var(--mx-md); - } - .md\:bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .md\:bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .md\:bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .md\:bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .md\:bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .md\:bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .md\:bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .md\:bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .md\:bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .md\:bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .md\:bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .md\:bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .md\:bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .md\:bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .md\:bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .md\:bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-mx { - margin-left: var(--mx-lg); - margin-right: var(--mx-lg); - } - .lg\:bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .lg\:bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .lg\:bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .lg\:bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .lg\:bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .lg\:bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .lg\:bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .lg\:bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .lg\:bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .lg\:bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .lg\:bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .lg\:bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .lg\:bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .lg\:bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .lg\:bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .lg\:bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-mx { - margin-left: var(--mx-xl); - margin-right: var(--mx-xl); - } - .xl\:bui-mx-0\.5 { - margin-left: var(--bui-space-0_5); - margin-right: var(--bui-space-0_5); - } - .xl\:bui-mx-1 { - margin-left: var(--bui-space-1); - margin-right: var(--bui-space-1); - } - .xl\:bui-mx-1\.5 { - margin-left: var(--bui-space-1_5); - margin-right: var(--bui-space-1_5); - } - .xl\:bui-mx-2 { - margin-left: var(--bui-space-2); - margin-right: var(--bui-space-2); - } - .xl\:bui-mx-3 { - margin-left: var(--bui-space-3); - margin-right: var(--bui-space-3); - } - .xl\:bui-mx-4 { - margin-left: var(--bui-space-4); - margin-right: var(--bui-space-4); - } - .xl\:bui-mx-5 { - margin-left: var(--bui-space-5); - margin-right: var(--bui-space-5); - } - .xl\:bui-mx-6 { - margin-left: var(--bui-space-6); - margin-right: var(--bui-space-6); - } - .xl\:bui-mx-7 { - margin-left: var(--bui-space-7); - margin-right: var(--bui-space-7); - } - .xl\:bui-mx-8 { - margin-left: var(--bui-space-8); - margin-right: var(--bui-space-8); - } - .xl\:bui-mx-9 { - margin-left: var(--bui-space-9); - margin-right: var(--bui-space-9); - } - .xl\:bui-mx-10 { - margin-left: var(--bui-space-10); - margin-right: var(--bui-space-10); - } - .xl\:bui-mx-11 { - margin-left: var(--bui-space-11); - margin-right: var(--bui-space-11); - } - .xl\:bui-mx-12 { - margin-left: var(--bui-space-12); - margin-right: var(--bui-space-12); - } - .xl\:bui-mx-13 { - margin-left: var(--bui-space-13); - margin-right: var(--bui-space-13); - } - .xl\:bui-mx-14 { - margin-left: var(--bui-space-14); - margin-right: var(--bui-space-14); - } - } - .bui-display-none { - display: none; - } - .bui-display-inline { - display: inline; - } - .bui-display-inline-block { - display: inline-block; - } - .bui-display-block { - display: block; - } - @media (width>=640px) { - .xs\:bui-display-none { - display: none; - } - .xs\:bui-display-inline { - display: inline; - } - .xs\:bui-display-inline-block { - display: inline-block; - } - .xs\:bui-display-block { - display: block; - } - } - @media (width>=768px) { - .sm\:bui-display-none { - display: none; - } - .sm\:bui-display-inline { - display: inline; - } - .sm\:bui-display-inline-block { - display: inline-block; - } - .sm\:bui-display-block { - display: block; - } - } - @media (width>=1024px) { - .md\:bui-display-none { - display: none; - } - .md\:bui-display-inline { - display: inline; - } - .md\:bui-display-inline-block { - display: inline-block; - } - .md\:bui-display-block { - display: block; - } - } - @media (width>=1280px) { - .lg\:bui-display-none { - display: none; - } - .lg\:bui-display-inline { - display: inline; - } - .lg\:bui-display-inline-block { - display: inline-block; - } - .lg\:bui-display-block { - display: block; - } - } - @media (width>=1536px) { - .xl\:bui-display-none { - display: none; - } - .xl\:bui-display-inline { - display: inline; - } - .xl\:bui-display-inline-block { - display: inline-block; - } - .xl\:bui-display-block { - display: block; - } - } - .bui-w { - width: var(--width); - } - .bui-min-w { - min-width: var(--min-width); - } - .bui-max-w { - max-width: var(--max-width); - } - @media (width>=640px) { - .xs\:bui-w { - width: var(--width); - } - .xs\:bui-min-w { - min-width: var(--min-width); - } - .xs\:bui-max-w { - max-width: var(--max-width); - } - } - @media (width>=768px) { - .sm\:bui-w { - width: var(--width); - } - .sm\:bui-min-w { - min-width: var(--min-width); - } - .sm\:bui-max-w { - max-width: var(--max-width); - } - } - @media (width>=1024px) { - .md\:bui-w { - width: var(--width); - } - .md\:bui-min-w { - min-width: var(--min-width); - } - .md\:bui-max-w { - max-width: var(--max-width); - } - } - @media (width>=1280px) { - .lg\:bui-w { - width: var(--width); - } - .lg\:bui-min-w { - min-width: var(--min-width); - } - .lg\:bui-max-w { - max-width: var(--max-width); - } - } - @media (width>=1536px) { - .xl\:bui-w { - width: var(--width); - } - .xl\:bui-min-w { - min-width: var(--min-width); - } - .xl\:bui-max-w { - max-width: var(--max-width); - } - } - .bui-h { - height: var(--height); - } - .bui-min-h { - min-height: var(--min-height); - } - .bui-max-h { - max-height: var(--max-height); - } - @media (width>=640px) { - .xs\:bui-h { - height: var(--height); - } - .xs\:bui-min-h { - min-height: var(--min-height); - } - .xs\:bui-max-h { - max-height: var(--max-height); - } - } - @media (width>=768px) { - .sm\:bui-h { - height: var(--height); - } - .sm\:bui-min-h { - min-height: var(--min-height); - } - .sm\:bui-max-h { - max-height: var(--max-height); - } - } - @media (width>=1024px) { - .md\:bui-h { - height: var(--height); - } - .md\:bui-min-h { - min-height: var(--min-height); - } - .md\:bui-max-h { - max-height: var(--max-height); - } - } - @media (width>=1280px) { - .lg\:bui-h { - height: var(--height); - } - .lg\:bui-min-h { - min-height: var(--min-height); - } - .lg\:bui-max-h { - max-height: var(--max-height); - } - } - @media (width>=1536px) { - .xl\:bui-h { - height: var(--height); - } - .xl\:bui-min-h { - min-height: var(--min-height); - } - .xl\:bui-max-h { - max-height: var(--max-height); - } - } - .bui-position-absolute { - position: absolute; - } - .bui-position-fixed { - position: fixed; - } - .bui-position-sticky { - position: sticky; - } - .bui-position-relative { - position: relative; - } - .bui-position-static { - position: static; - } - @media (width>=640px) { - .xs\:bui-position-absolute { - position: absolute; - } - .xs\:bui-position-fixed { - position: fixed; - } - .xs\:bui-position-sticky { - position: sticky; - } - .xs\:bui-position-relative { - position: relative; - } - .xs\:bui-position-static { - position: static; - } - } - @media (width>=768px) { - .sm\:bui-position-absolute { - position: absolute; - } - .sm\:bui-position-fixed { - position: fixed; - } - .sm\:bui-position-sticky { - position: sticky; - } - .sm\:bui-position-relative { - position: relative; - } - .sm\:bui-position-static { - position: static; - } - } - @media (width>=1024px) { - .md\:bui-position-absolute { - position: absolute; - } - .md\:bui-position-fixed { - position: fixed; - } - .md\:bui-position-sticky { - position: sticky; - } - .md\:bui-position-relative { - position: relative; - } - .md\:bui-position-static { - position: static; - } - } - @media (width>=1280px) { - .lg\:bui-position-absolute { - position: absolute; - } - .lg\:bui-position-fixed { - position: fixed; - } - .lg\:bui-position-sticky { - position: sticky; - } - .lg\:bui-position-relative { - position: relative; - } - .lg\:bui-position-static { - position: static; - } - } - @media (width>=1536px) { - .xl\:bui-position-absolute { - position: absolute; - } - .xl\:bui-position-fixed { - position: fixed; - } - .xl\:bui-position-sticky { - position: sticky; - } - .xl\:bui-position-relative { - position: relative; - } - .xl\:bui-position-static { - position: static; - } - } - .bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .bui-col-span-1 { - grid-column: span 1 / span 1; - } - .bui-col-span-2 { - grid-column: span 2 / span 2; - } - .bui-col-span-3 { - grid-column: span 3 / span 3; - } - .bui-col-span-4 { - grid-column: span 4 / span 4; - } - .bui-col-span-5 { - grid-column: span 5 / span 5; - } - .bui-col-span-6 { - grid-column: span 6 / span 6; - } - .bui-col-span-7 { - grid-column: span 7 / span 7; - } - .bui-col-span-8 { - grid-column: span 8 / span 8; - } - .bui-col-span-9 { - grid-column: span 9 / span 9; - } - .bui-col-span-10 { - grid-column: span 10 / span 10; - } - .bui-col-span-11 { - grid-column: span 11 / span 11; - } - .bui-col-span-12 { - grid-column: span 12 / span 12; - } - .bui-col-span-auto { - grid-column: span auto/span auto; - } - .bui-col-start-1 { - grid-column-start: 1; - } - .bui-col-start-2 { - grid-column-start: 2; - } - .bui-col-start-3 { - grid-column-start: 3; - } - .bui-col-start-4 { - grid-column-start: 4; - } - .bui-col-start-5 { - grid-column-start: 5; - } - .bui-col-start-6 { - grid-column-start: 6; - } - .bui-col-start-7 { - grid-column-start: 7; - } - .bui-col-start-8 { - grid-column-start: 8; - } - .bui-col-start-9 { - grid-column-start: 9; - } - .bui-col-start-10 { - grid-column-start: 10; - } - .bui-col-start-11 { - grid-column-start: 11; - } - .bui-col-start-12 { - grid-column-start: 12; - } - .bui-col-start-13 { - grid-column-start: 13; - } - .bui-col-start-auto { - grid-column-start: auto; - } - .bui-col-end-1 { - grid-column-end: 1; - } - .bui-col-end-2 { - grid-column-end: 2; - } - .bui-col-end-3 { - grid-column-end: 3; - } - .bui-col-end-4 { - grid-column-end: 4; - } - .bui-col-end-5 { - grid-column-end: 5; - } - .bui-col-end-6 { - grid-column-end: 6; - } - .bui-col-end-7 { - grid-column-end: 7; - } - .bui-col-end-8 { - grid-column-end: 8; - } - .bui-col-end-9 { - grid-column-end: 9; - } - .bui-col-end-10 { - grid-column-end: 10; - } - .bui-col-end-11 { - grid-column-end: 11; - } - .bui-col-end-12 { - grid-column-end: 12; - } - .bui-col-end-13 { - grid-column-end: 13; - } - .bui-col-end-auto { - grid-column-end: auto; - } - .bui-row-span-1 { - grid-row: span 1 / span 1; - } - .bui-row-span-2 { - grid-row: span 2 / span 2; - } - .bui-row-span-3 { - grid-row: span 3 / span 3; - } - .bui-row-span-4 { - grid-row: span 4 / span 4; - } - .bui-row-span-5 { - grid-row: span 5 / span 5; - } - .bui-row-span-6 { - grid-row: span 6 / span 6; - } - .bui-row-span-7 { - grid-row: span 7 / span 7; - } - .bui-row-span-8 { - grid-row: span 8 / span 8; - } - .bui-row-span-9 { - grid-row: span 9 / span 9; - } - .bui-row-span-10 { - grid-row: span 10 / span 10; - } - .bui-row-span-11 { - grid-row: span 11 / span 11; - } - .bui-row-span-12 { - grid-row: span 12 / span 12; - } - .bui-row-span-auto { - grid-row: span auto/span auto; - } - @media (width>=640px) { - .xs\:bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .xs\:bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .xs\:bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .xs\:bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .xs\:bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .xs\:bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .xs\:bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .xs\:bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .xs\:bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .xs\:bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .xs\:bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .xs\:bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .xs\:bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .xs\:bui-col-span-1 { - grid-column: span 1 / span 1; - } - .xs\:bui-col-span-2 { - grid-column: span 2 / span 2; - } - .xs\:bui-col-span-3 { - grid-column: span 3 / span 3; - } - .xs\:bui-col-span-4 { - grid-column: span 4 / span 4; - } - .xs\:bui-col-span-5 { - grid-column: span 5 / span 5; - } - .xs\:bui-col-span-6 { - grid-column: span 6 / span 6; - } - .xs\:bui-col-span-7 { - grid-column: span 7 / span 7; - } - .xs\:bui-col-span-8 { - grid-column: span 8 / span 8; - } - .xs\:bui-col-span-9 { - grid-column: span 9 / span 9; - } - .xs\:bui-col-span-10 { - grid-column: span 10 / span 10; - } - .xs\:bui-col-span-11 { - grid-column: span 11 / span 11; - } - .xs\:bui-col-span-12 { - grid-column: span 12 / span 12; - } - .xs\:bui-col-span-auto { - grid-column: span auto/span auto; - } - .xs\:bui-col-start-1 { - grid-column-start: 1; - } - .xs\:bui-col-start-2 { - grid-column-start: 2; - } - .xs\:bui-col-start-3 { - grid-column-start: 3; - } - .xs\:bui-col-start-4 { - grid-column-start: 4; - } - .xs\:bui-col-start-5 { - grid-column-start: 5; - } - .xs\:bui-col-start-6 { - grid-column-start: 6; - } - .xs\:bui-col-start-7 { - grid-column-start: 7; - } - .xs\:bui-col-start-8 { - grid-column-start: 8; - } - .xs\:bui-col-start-9 { - grid-column-start: 9; - } - .xs\:bui-col-start-10 { - grid-column-start: 10; - } - .xs\:bui-col-start-11 { - grid-column-start: 11; - } - .xs\:bui-col-start-12 { - grid-column-start: 12; - } - .xs\:bui-col-start-13 { - grid-column-start: 13; - } - .xs\:bui-col-start-auto { - grid-column-start: auto; - } - .xs\:bui-col-end-1 { - grid-column-end: 1; - } - .xs\:bui-col-end-2 { - grid-column-end: 2; - } - .xs\:bui-col-end-3 { - grid-column-end: 3; - } - .xs\:bui-col-end-4 { - grid-column-end: 4; - } - .xs\:bui-col-end-5 { - grid-column-end: 5; - } - .xs\:bui-col-end-6 { - grid-column-end: 6; - } - .xs\:bui-col-end-7 { - grid-column-end: 7; - } - .xs\:bui-col-end-8 { - grid-column-end: 8; - } - .xs\:bui-col-end-9 { - grid-column-end: 9; - } - .xs\:bui-col-end-10 { - grid-column-end: 10; - } - .xs\:bui-col-end-11 { - grid-column-end: 11; - } - .xs\:bui-col-end-12 { - grid-column-end: 12; - } - .xs\:bui-col-end-13 { - grid-column-end: 13; - } - .xs\:bui-col-end-auto { - grid-column-end: auto; - } - .xs\:bui-row-span-1 { - grid-row: span 1 / span 1; - } - .xs\:bui-row-span-2 { - grid-row: span 2 / span 2; - } - .xs\:bui-row-span-3 { - grid-row: span 3 / span 3; - } - .xs\:bui-row-span-4 { - grid-row: span 4 / span 4; - } - .xs\:bui-row-span-5 { - grid-row: span 5 / span 5; - } - .xs\:bui-row-span-6 { - grid-row: span 6 / span 6; - } - .xs\:bui-row-span-7 { - grid-row: span 7 / span 7; - } - .xs\:bui-row-span-8 { - grid-row: span 8 / span 8; - } - .xs\:bui-row-span-9 { - grid-row: span 9 / span 9; - } - .xs\:bui-row-span-10 { - grid-row: span 10 / span 10; - } - .xs\:bui-row-span-11 { - grid-row: span 11 / span 11; - } - .xs\:bui-row-span-12 { - grid-row: span 12 / span 12; - } - .xs\:bui-row-span-auto { - grid-row: span auto/span auto; - } - } - @media (width>=768px) { - .sm\:bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .sm\:bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .sm\:bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .sm\:bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .sm\:bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .sm\:bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .sm\:bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .sm\:bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .sm\:bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .sm\:bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .sm\:bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .sm\:bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .sm\:bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .sm\:bui-col-span-1 { - grid-column: span 1 / span 1; - } - .sm\:bui-col-span-2 { - grid-column: span 2 / span 2; - } - .sm\:bui-col-span-3 { - grid-column: span 3 / span 3; - } - .sm\:bui-col-span-4 { - grid-column: span 4 / span 4; - } - .sm\:bui-col-span-5 { - grid-column: span 5 / span 5; - } - .sm\:bui-col-span-6 { - grid-column: span 6 / span 6; - } - .sm\:bui-col-span-7 { - grid-column: span 7 / span 7; - } - .sm\:bui-col-span-8 { - grid-column: span 8 / span 8; - } - .sm\:bui-col-span-9 { - grid-column: span 9 / span 9; - } - .sm\:bui-col-span-10 { - grid-column: span 10 / span 10; - } - .sm\:bui-col-span-11 { - grid-column: span 11 / span 11; - } - .sm\:bui-col-span-12 { - grid-column: span 12 / span 12; - } - .sm\:bui-col-span-auto { - grid-column: span auto/span auto; - } - .sm\:bui-col-start-1 { - grid-column-start: 1; - } - .sm\:bui-col-start-2 { - grid-column-start: 2; - } - .sm\:bui-col-start-3 { - grid-column-start: 3; - } - .sm\:bui-col-start-4 { - grid-column-start: 4; - } - .sm\:bui-col-start-5 { - grid-column-start: 5; - } - .sm\:bui-col-start-6 { - grid-column-start: 6; - } - .sm\:bui-col-start-7 { - grid-column-start: 7; - } - .sm\:bui-col-start-8 { - grid-column-start: 8; - } - .sm\:bui-col-start-9 { - grid-column-start: 9; - } - .sm\:bui-col-start-10 { - grid-column-start: 10; - } - .sm\:bui-col-start-11 { - grid-column-start: 11; - } - .sm\:bui-col-start-12 { - grid-column-start: 12; - } - .sm\:bui-col-start-13 { - grid-column-start: 13; - } - .sm\:bui-col-start-auto { - grid-column-start: auto; - } - .sm\:bui-col-end-1 { - grid-column-end: 1; - } - .sm\:bui-col-end-2 { - grid-column-end: 2; - } - .sm\:bui-col-end-3 { - grid-column-end: 3; - } - .sm\:bui-col-end-4 { - grid-column-end: 4; - } - .sm\:bui-col-end-5 { - grid-column-end: 5; - } - .sm\:bui-col-end-6 { - grid-column-end: 6; - } - .sm\:bui-col-end-7 { - grid-column-end: 7; - } - .sm\:bui-col-end-8 { - grid-column-end: 8; - } - .sm\:bui-col-end-9 { - grid-column-end: 9; - } - .sm\:bui-col-end-10 { - grid-column-end: 10; - } - .sm\:bui-col-end-11 { - grid-column-end: 11; - } - .sm\:bui-col-end-12 { - grid-column-end: 12; - } - .sm\:bui-col-end-13 { - grid-column-end: 13; - } - .sm\:bui-col-end-auto { - grid-column-end: auto; - } - .sm\:bui-row-span-1 { - grid-row: span 1 / span 1; - } - .sm\:bui-row-span-2 { - grid-row: span 2 / span 2; - } - .sm\:bui-row-span-3 { - grid-row: span 3 / span 3; - } - .sm\:bui-row-span-4 { - grid-row: span 4 / span 4; - } - .sm\:bui-row-span-5 { - grid-row: span 5 / span 5; - } - .sm\:bui-row-span-6 { - grid-row: span 6 / span 6; - } - .sm\:bui-row-span-7 { - grid-row: span 7 / span 7; - } - .sm\:bui-row-span-8 { - grid-row: span 8 / span 8; - } - .sm\:bui-row-span-9 { - grid-row: span 9 / span 9; - } - .sm\:bui-row-span-10 { - grid-row: span 10 / span 10; - } - .sm\:bui-row-span-11 { - grid-row: span 11 / span 11; - } - .sm\:bui-row-span-12 { - grid-row: span 12 / span 12; - } - .sm\:bui-row-span-auto { - grid-row: span auto/span auto; - } - } - @media (width>=1024px) { - .md\:bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .md\:bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .md\:bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .md\:bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .md\:bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .md\:bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .md\:bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .md\:bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .md\:bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .md\:bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .md\:bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .md\:bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .md\:bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .md\:bui-col-span-1 { - grid-column: span 1 / span 1; - } - .md\:bui-col-span-2 { - grid-column: span 2 / span 2; - } - .md\:bui-col-span-3 { - grid-column: span 3 / span 3; - } - .md\:bui-col-span-4 { - grid-column: span 4 / span 4; - } - .md\:bui-col-span-5 { - grid-column: span 5 / span 5; - } - .md\:bui-col-span-6 { - grid-column: span 6 / span 6; - } - .md\:bui-col-span-7 { - grid-column: span 7 / span 7; - } - .md\:bui-col-span-8 { - grid-column: span 8 / span 8; - } - .md\:bui-col-span-9 { - grid-column: span 9 / span 9; - } - .md\:bui-col-span-10 { - grid-column: span 10 / span 10; - } - .md\:bui-col-span-11 { - grid-column: span 11 / span 11; - } - .md\:bui-col-span-12 { - grid-column: span 12 / span 12; - } - .md\:bui-col-span-auto { - grid-column: span auto/span auto; - } - .md\:bui-col-start-1 { - grid-column-start: 1; - } - .md\:bui-col-start-2 { - grid-column-start: 2; - } - .md\:bui-col-start-3 { - grid-column-start: 3; - } - .md\:bui-col-start-4 { - grid-column-start: 4; - } - .md\:bui-col-start-5 { - grid-column-start: 5; - } - .md\:bui-col-start-6 { - grid-column-start: 6; - } - .md\:bui-col-start-7 { - grid-column-start: 7; - } - .md\:bui-col-start-8 { - grid-column-start: 8; - } - .md\:bui-col-start-9 { - grid-column-start: 9; - } - .md\:bui-col-start-10 { - grid-column-start: 10; - } - .md\:bui-col-start-11 { - grid-column-start: 11; - } - .md\:bui-col-start-12 { - grid-column-start: 12; - } - .md\:bui-col-start-13 { - grid-column-start: 13; - } - .md\:bui-col-start-auto { - grid-column-start: auto; - } - .md\:bui-col-end-1 { - grid-column-end: 1; - } - .md\:bui-col-end-2 { - grid-column-end: 2; - } - .md\:bui-col-end-3 { - grid-column-end: 3; - } - .md\:bui-col-end-4 { - grid-column-end: 4; - } - .md\:bui-col-end-5 { - grid-column-end: 5; - } - .md\:bui-col-end-6 { - grid-column-end: 6; - } - .md\:bui-col-end-7 { - grid-column-end: 7; - } - .md\:bui-col-end-8 { - grid-column-end: 8; - } - .md\:bui-col-end-9 { - grid-column-end: 9; - } - .md\:bui-col-end-10 { - grid-column-end: 10; - } - .md\:bui-col-end-11 { - grid-column-end: 11; - } - .md\:bui-col-end-12 { - grid-column-end: 12; - } - .md\:bui-col-end-13 { - grid-column-end: 13; - } - .md\:bui-col-end-auto { - grid-column-end: auto; - } - .md\:bui-row-span-1 { - grid-row: span 1 / span 1; - } - .md\:bui-row-span-2 { - grid-row: span 2 / span 2; - } - .md\:bui-row-span-3 { - grid-row: span 3 / span 3; - } - .md\:bui-row-span-4 { - grid-row: span 4 / span 4; - } - .md\:bui-row-span-5 { - grid-row: span 5 / span 5; - } - .md\:bui-row-span-6 { - grid-row: span 6 / span 6; - } - .md\:bui-row-span-7 { - grid-row: span 7 / span 7; - } - .md\:bui-row-span-8 { - grid-row: span 8 / span 8; - } - .md\:bui-row-span-9 { - grid-row: span 9 / span 9; - } - .md\:bui-row-span-10 { - grid-row: span 10 / span 10; - } - .md\:bui-row-span-11 { - grid-row: span 11 / span 11; - } - .md\:bui-row-span-12 { - grid-row: span 12 / span 12; - } - .md\:bui-row-span-auto { - grid-row: span auto/span auto; - } - } - @media (width>=1280px) { - .lg\:bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .lg\:bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .lg\:bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .lg\:bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .lg\:bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .lg\:bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .lg\:bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .lg\:bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .lg\:bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .lg\:bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .lg\:bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .lg\:bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .lg\:bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .lg\:bui-col-span-1 { - grid-column: span 1 / span 1; - } - .lg\:bui-col-span-2 { - grid-column: span 2 / span 2; - } - .lg\:bui-col-span-3 { - grid-column: span 3 / span 3; - } - .lg\:bui-col-span-4 { - grid-column: span 4 / span 4; - } - .lg\:bui-col-span-5 { - grid-column: span 5 / span 5; - } - .lg\:bui-col-span-6 { - grid-column: span 6 / span 6; - } - .lg\:bui-col-span-7 { - grid-column: span 7 / span 7; - } - .lg\:bui-col-span-8 { - grid-column: span 8 / span 8; - } - .lg\:bui-col-span-9 { - grid-column: span 9 / span 9; - } - .lg\:bui-col-span-10 { - grid-column: span 10 / span 10; - } - .lg\:bui-col-span-11 { - grid-column: span 11 / span 11; - } - .lg\:bui-col-span-12 { - grid-column: span 12 / span 12; - } - .lg\:bui-col-span-auto { - grid-column: span auto/span auto; - } - .lg\:bui-col-start-1 { - grid-column-start: 1; - } - .lg\:bui-col-start-2 { - grid-column-start: 2; - } - .lg\:bui-col-start-3 { - grid-column-start: 3; - } - .lg\:bui-col-start-4 { - grid-column-start: 4; - } - .lg\:bui-col-start-5 { - grid-column-start: 5; - } - .lg\:bui-col-start-6 { - grid-column-start: 6; - } - .lg\:bui-col-start-7 { - grid-column-start: 7; - } - .lg\:bui-col-start-8 { - grid-column-start: 8; - } - .lg\:bui-col-start-9 { - grid-column-start: 9; - } - .lg\:bui-col-start-10 { - grid-column-start: 10; - } - .lg\:bui-col-start-11 { - grid-column-start: 11; - } - .lg\:bui-col-start-12 { - grid-column-start: 12; - } - .lg\:bui-col-start-13 { - grid-column-start: 13; - } - .lg\:bui-col-start-auto { - grid-column-start: auto; - } - .lg\:bui-col-end-1 { - grid-column-end: 1; - } - .lg\:bui-col-end-2 { - grid-column-end: 2; - } - .lg\:bui-col-end-3 { - grid-column-end: 3; - } - .lg\:bui-col-end-4 { - grid-column-end: 4; - } - .lg\:bui-col-end-5 { - grid-column-end: 5; - } - .lg\:bui-col-end-6 { - grid-column-end: 6; - } - .lg\:bui-col-end-7 { - grid-column-end: 7; - } - .lg\:bui-col-end-8 { - grid-column-end: 8; - } - .lg\:bui-col-end-9 { - grid-column-end: 9; - } - .lg\:bui-col-end-10 { - grid-column-end: 10; - } - .lg\:bui-col-end-11 { - grid-column-end: 11; - } - .lg\:bui-col-end-12 { - grid-column-end: 12; - } - .lg\:bui-col-end-13 { - grid-column-end: 13; - } - .lg\:bui-col-end-auto { - grid-column-end: auto; - } - .lg\:bui-row-span-1 { - grid-row: span 1 / span 1; - } - .lg\:bui-row-span-2 { - grid-row: span 2 / span 2; - } - .lg\:bui-row-span-3 { - grid-row: span 3 / span 3; - } - .lg\:bui-row-span-4 { - grid-row: span 4 / span 4; - } - .lg\:bui-row-span-5 { - grid-row: span 5 / span 5; - } - .lg\:bui-row-span-6 { - grid-row: span 6 / span 6; - } - .lg\:bui-row-span-7 { - grid-row: span 7 / span 7; - } - .lg\:bui-row-span-8 { - grid-row: span 8 / span 8; - } - .lg\:bui-row-span-9 { - grid-row: span 9 / span 9; - } - .lg\:bui-row-span-10 { - grid-row: span 10 / span 10; - } - .lg\:bui-row-span-11 { - grid-row: span 11 / span 11; - } - .lg\:bui-row-span-12 { - grid-row: span 12 / span 12; - } - .lg\:bui-row-span-auto { - grid-row: span auto/span auto; - } - } - @media (width>=1536px) { - .xl\:bui-columns-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .xl\:bui-columns-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .xl\:bui-columns-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .xl\:bui-columns-4 { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - .xl\:bui-columns-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .xl\:bui-columns-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .xl\:bui-columns-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); - } - .xl\:bui-columns-8 { - grid-template-columns: repeat(8, minmax(0, 1fr)); - } - .xl\:bui-columns-9 { - grid-template-columns: repeat(9, minmax(0, 1fr)); - } - .xl\:bui-columns-10 { - grid-template-columns: repeat(10, minmax(0, 1fr)); - } - .xl\:bui-columns-11 { - grid-template-columns: repeat(11, minmax(0, 1fr)); - } - .xl\:bui-columns-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } - .xl\:bui-columns-auto { - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .xl\:bui-col-span-1 { - grid-column: span 1 / span 1; - } - .xl\:bui-col-span-2 { - grid-column: span 2 / span 2; - } - .xl\:bui-col-span-3 { - grid-column: span 3 / span 3; - } - .xl\:bui-col-span-4 { - grid-column: span 4 / span 4; - } - .xl\:bui-col-span-5 { - grid-column: span 5 / span 5; - } - .xl\:bui-col-span-6 { - grid-column: span 6 / span 6; - } - .xl\:bui-col-span-7 { - grid-column: span 7 / span 7; - } - .xl\:bui-col-span-8 { - grid-column: span 8 / span 8; - } - .xl\:bui-col-span-9 { - grid-column: span 9 / span 9; - } - .xl\:bui-col-span-10 { - grid-column: span 10 / span 10; - } - .xl\:bui-col-span-11 { - grid-column: span 11 / span 11; - } - .xl\:bui-col-span-12 { - grid-column: span 12 / span 12; - } - .xl\:bui-col-span-auto { - grid-column: span auto/span auto; - } - .xl\:bui-col-start-1 { - grid-column-start: 1; - } - .xl\:bui-col-start-2 { - grid-column-start: 2; - } - .xl\:bui-col-start-3 { - grid-column-start: 3; - } - .xl\:bui-col-start-4 { - grid-column-start: 4; - } - .xl\:bui-col-start-5 { - grid-column-start: 5; - } - .xl\:bui-col-start-6 { - grid-column-start: 6; - } - .xl\:bui-col-start-7 { - grid-column-start: 7; - } - .xl\:bui-col-start-8 { - grid-column-start: 8; - } - .xl\:bui-col-start-9 { - grid-column-start: 9; - } - .xl\:bui-col-start-10 { - grid-column-start: 10; - } - .xl\:bui-col-start-11 { - grid-column-start: 11; - } - .xl\:bui-col-start-12 { - grid-column-start: 12; - } - .xl\:bui-col-start-13 { - grid-column-start: 13; - } - .xl\:bui-col-start-auto { - grid-column-start: auto; - } - .xl\:bui-col-end-1 { - grid-column-end: 1; - } - .xl\:bui-col-end-2 { - grid-column-end: 2; - } - .xl\:bui-col-end-3 { - grid-column-end: 3; - } - .xl\:bui-col-end-4 { - grid-column-end: 4; - } - .xl\:bui-col-end-5 { - grid-column-end: 5; - } - .xl\:bui-col-end-6 { - grid-column-end: 6; - } - .xl\:bui-col-end-7 { - grid-column-end: 7; - } - .xl\:bui-col-end-8 { - grid-column-end: 8; - } - .xl\:bui-col-end-9 { - grid-column-end: 9; - } - .xl\:bui-col-end-10 { - grid-column-end: 10; - } - .xl\:bui-col-end-11 { - grid-column-end: 11; - } - .xl\:bui-col-end-12 { - grid-column-end: 12; - } - .xl\:bui-col-end-13 { - grid-column-end: 13; - } - .xl\:bui-col-end-auto { - grid-column-end: auto; - } - .xl\:bui-row-span-1 { - grid-row: span 1 / span 1; - } - .xl\:bui-row-span-2 { - grid-row: span 2 / span 2; - } - .xl\:bui-row-span-3 { - grid-row: span 3 / span 3; - } - .xl\:bui-row-span-4 { - grid-row: span 4 / span 4; - } - .xl\:bui-row-span-5 { - grid-row: span 5 / span 5; - } - .xl\:bui-row-span-6 { - grid-row: span 6 / span 6; - } - .xl\:bui-row-span-7 { - grid-row: span 7 / span 7; - } - .xl\:bui-row-span-8 { - grid-row: span 8 / span 8; - } - .xl\:bui-row-span-9 { - grid-row: span 9 / span 9; - } - .xl\:bui-row-span-10 { - grid-row: span 10 / span 10; - } - .xl\:bui-row-span-11 { - grid-row: span 11 / span 11; - } - .xl\:bui-row-span-12 { - grid-row: span 12 / span 12; - } - .xl\:bui-row-span-auto { - grid-row: span auto/span auto; - } - } - .bui-gap { - gap: var(--gap); - } - .bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .bui-gap-1 { - gap: var(--bui-space-1); - } - .bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .bui-gap-2 { - gap: var(--bui-space-2); - } - .bui-gap-3 { - gap: var(--bui-space-3); - } - .bui-gap-4 { - gap: var(--bui-space-4); - } - .bui-gap-5 { - gap: var(--bui-space-5); - } - .bui-gap-6 { - gap: var(--bui-space-6); - } - .bui-gap-7 { - gap: var(--bui-space-7); - } - .bui-gap-8 { - gap: var(--bui-space-8); - } - .bui-gap-9 { - gap: var(--bui-space-9); - } - .bui-gap-10 { - gap: var(--bui-space-10); - } - .bui-gap-11 { - gap: var(--bui-space-11); - } - .bui-gap-12 { - gap: var(--bui-space-12); - } - .bui-gap-13 { - gap: var(--bui-space-13); - } - .bui-gap-14 { - gap: var(--bui-space-14); - } - @media (width>=640px) { - .xs\:bui-gap { - gap: var(--gap-xs); - } - .xs\:bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .xs\:bui-gap-1 { - gap: var(--bui-space-1); - } - .xs\:bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .xs\:bui-gap-2 { - gap: var(--bui-space-2); - } - .xs\:bui-gap-3 { - gap: var(--bui-space-3); - } - .xs\:bui-gap-4 { - gap: var(--bui-space-4); - } - .xs\:bui-gap-5 { - gap: var(--bui-space-5); - } - .xs\:bui-gap-6 { - gap: var(--bui-space-6); - } - .xs\:bui-gap-7 { - gap: var(--bui-space-7); - } - .xs\:bui-gap-8 { - gap: var(--bui-space-8); - } - .xs\:bui-gap-9 { - gap: var(--bui-space-9); - } - .xs\:bui-gap-10 { - gap: var(--bui-space-10); - } - .xs\:bui-gap-11 { - gap: var(--bui-space-11); - } - .xs\:bui-gap-12 { - gap: var(--bui-space-12); - } - .xs\:bui-gap-13 { - gap: var(--bui-space-13); - } - .xs\:bui-gap-14 { - gap: var(--bui-space-14); - } - } - @media (width>=768px) { - .sm\:bui-gap { - gap: var(--gap-sm); - } - .sm\:bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .sm\:bui-gap-1 { - gap: var(--bui-space-1); - } - .sm\:bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .sm\:bui-gap-2 { - gap: var(--bui-space-2); - } - .sm\:bui-gap-3 { - gap: var(--bui-space-3); - } - .sm\:bui-gap-4 { - gap: var(--bui-space-4); - } - .sm\:bui-gap-5 { - gap: var(--bui-space-5); - } - .sm\:bui-gap-6 { - gap: var(--bui-space-6); - } - .sm\:bui-gap-7 { - gap: var(--bui-space-7); - } - .sm\:bui-gap-8 { - gap: var(--bui-space-8); - } - .sm\:bui-gap-9 { - gap: var(--bui-space-9); - } - .sm\:bui-gap-10 { - gap: var(--bui-space-10); - } - .sm\:bui-gap-11 { - gap: var(--bui-space-11); - } - .sm\:bui-gap-12 { - gap: var(--bui-space-12); - } - .sm\:bui-gap-13 { - gap: var(--bui-space-13); - } - .sm\:bui-gap-14 { - gap: var(--bui-space-14); - } - } - @media (width>=1024px) { - .md\:bui-gap { - gap: var(--gap-md); - } - .md\:bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .md\:bui-gap-1 { - gap: var(--bui-space-1); - } - .md\:bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .md\:bui-gap-2 { - gap: var(--bui-space-2); - } - .md\:bui-gap-3 { - gap: var(--bui-space-3); - } - .md\:bui-gap-4 { - gap: var(--bui-space-4); - } - .md\:bui-gap-5 { - gap: var(--bui-space-5); - } - .md\:bui-gap-6 { - gap: var(--bui-space-6); - } - .md\:bui-gap-7 { - gap: var(--bui-space-7); - } - .md\:bui-gap-8 { - gap: var(--bui-space-8); - } - .md\:bui-gap-9 { - gap: var(--bui-space-9); - } - .md\:bui-gap-10 { - gap: var(--bui-space-10); - } - .md\:bui-gap-11 { - gap: var(--bui-space-11); - } - .md\:bui-gap-12 { - gap: var(--bui-space-12); - } - .md\:bui-gap-13 { - gap: var(--bui-space-13); - } - .md\:bui-gap-14 { - gap: var(--bui-space-14); - } - } - @media (width>=1280px) { - .lg\:bui-gap { - gap: var(--gap-lg); - } - .lg\:bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .lg\:bui-gap-1 { - gap: var(--bui-space-1); - } - .lg\:bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .lg\:bui-gap-2 { - gap: var(--bui-space-2); - } - .lg\:bui-gap-3 { - gap: var(--bui-space-3); - } - .lg\:bui-gap-4 { - gap: var(--bui-space-4); - } - .lg\:bui-gap-5 { - gap: var(--bui-space-5); - } - .lg\:bui-gap-6 { - gap: var(--bui-space-6); - } - .lg\:bui-gap-7 { - gap: var(--bui-space-7); - } - .lg\:bui-gap-8 { - gap: var(--bui-space-8); - } - .lg\:bui-gap-9 { - gap: var(--bui-space-9); - } - .lg\:bui-gap-10 { - gap: var(--bui-space-10); - } - .lg\:bui-gap-11 { - gap: var(--bui-space-11); - } - .lg\:bui-gap-12 { - gap: var(--bui-space-12); - } - .lg\:bui-gap-13 { - gap: var(--bui-space-13); - } - .lg\:bui-gap-14 { - gap: var(--bui-space-14); - } - } - @media (width>=1536px) { - .xl\:bui-gap { - gap: var(--gap-xl); - } - .xl\:bui-gap-0\.5 { - gap: var(--bui-space-0_5); - } - .xl\:bui-gap-1 { - gap: var(--bui-space-1); - } - .xl\:bui-gap-1\.5 { - gap: var(--bui-space-1_5); - } - .xl\:bui-gap-2 { - gap: var(--bui-space-2); - } - .xl\:bui-gap-3 { - gap: var(--bui-space-3); - } - .xl\:bui-gap-4 { - gap: var(--bui-space-4); - } - .xl\:bui-gap-5 { - gap: var(--bui-space-5); - } - .xl\:bui-gap-6 { - gap: var(--bui-space-6); - } - .xl\:bui-gap-7 { - gap: var(--bui-space-7); - } - .xl\:bui-gap-8 { - gap: var(--bui-space-8); - } - .xl\:bui-gap-9 { - gap: var(--bui-space-9); - } - .xl\:bui-gap-10 { - gap: var(--bui-space-10); - } - .xl\:bui-gap-11 { - gap: var(--bui-space-11); - } - .xl\:bui-gap-12 { - gap: var(--bui-space-12); - } - .xl\:bui-gap-13 { - gap: var(--bui-space-13); - } - .xl\:bui-gap-14 { - gap: var(--bui-space-14); - } - } - .bui-align-start { - align-items: start; - } - .bui-align-center { - align-items: center; - } - .bui-align-end { - align-items: end; - } - .bui-align-baseline { - align-items: baseline; - } - .bui-align-stretch { - align-items: stretch; - } - .bui-jc-start { - justify-content: start; - } - .bui-jc-center { - justify-content: center; - } - .bui-jc-end { - justify-content: end; - } - .bui-jc-between { - justify-content: space-between; - } - .bui-fd-row { - flex-direction: row; - } - .bui-fd-column { - flex-direction: column; - } - .bui-fd-row-reverse { - flex-direction: row-reverse; - } - .bui-fd-column-reverse { - flex-direction: column-reverse; - } - @media (width>=640px) { - .xs\:bui-align-start { - align-items: start; - } - .xs\:bui-align-center { - align-items: center; - } - .xs\:bui-align-end { - align-items: end; - } - .xs\:bui-align-stretch { - align-items: stretch; - } - .xs\:bui-jc-start { - justify-content: start; - } - .xs\:bui-jc-center { - justify-content: center; - } - .xs\:bui-jc-end { - justify-content: end; - } - .xs\:bui-jc-between { - justify-content: space-between; - } - .xs\:bui-fd-row { - flex-direction: row; - } - .xs\:bui-fd-column { - flex-direction: column; - } - .xs\:bui-fd-row-reverse { - flex-direction: row-reverse; - } - .xs\:bui-fd-column-reverse { - flex-direction: column-reverse; - } - } - @media (width>=768px) { - .sm\:bui-align-start { - align-items: start; - } - .sm\:bui-align-center { - align-items: center; - } - .sm\:bui-align-end { - align-items: end; - } - .sm\:bui-align-stretch { - align-items: stretch; - } - .sm\:bui-jc-start { - justify-content: start; - } - .sm\:bui-jc-center { - justify-content: center; - } - .sm\:bui-jc-end { - justify-content: end; - } - .sm\:bui-jc-between { - justify-content: space-between; - } - .sm\:bui-fd-row { - flex-direction: row; - } - .sm\:bui-fd-column { - flex-direction: column; - } - .sm\:bui-fd-row-reverse { - flex-direction: row-reverse; - } - .sm\:bui-fd-column-reverse { - flex-direction: column-reverse; - } - } - @media (width>=1024px) { - .md\:bui-align-start { - align-items: start; - } - .md\:bui-align-center { - align-items: center; - } - .md\:bui-align-end { - align-items: end; - } - .md\:bui-align-stretch { - align-items: stretch; - } - .md\:bui-jc-start { - justify-content: start; - } - .md\:bui-jc-center { - justify-content: center; - } - .md\:bui-jc-end { - justify-content: end; - } - .md\:bui-jc-between { - justify-content: space-between; - } - .md\:bui-fd-row { - flex-direction: row; - } - .md\:bui-fd-column { - flex-direction: column; - } - .md\:bui-fd-row-reverse { - flex-direction: row-reverse; - } - .md\:bui-fd-column-reverse { - flex-direction: column-reverse; - } - } - @media (width>=1280px) { - .lg\:bui-align-start { - align-items: start; - } - .lg\:bui-align-center { - align-items: center; - } - .lg\:bui-align-end { - align-items: end; - } - .lg\:bui-align-stretch { - align-items: stretch; - } - .lg\:bui-jc-start { - justify-content: start; - } - .lg\:bui-jc-center { - justify-content: center; - } - .lg\:bui-jc-end { - justify-content: end; - } - .lg\:bui-jc-between { - justify-content: space-between; - } - .lg\:bui-fd-row { - flex-direction: row; - } - .lg\:bui-fd-column { - flex-direction: column; - } - .lg\:bui-fd-row-reverse { - flex-direction: row-reverse; - } - .lg\:bui-fd-column-reverse { - flex-direction: column-reverse; - } - } - @media (width>=1536px) { - .xl\:bui-align-start { - align-items: start; - } - .xl\:bui-align-center { - align-items: center; - } - .xl\:bui-align-end { - align-items: end; - } - .xl\:bui-align-stretch { - align-items: stretch; - } - .xl\:bui-jc-start { - justify-content: start; - } - .xl\:bui-jc-center { - justify-content: center; - } - .xl\:bui-jc-end { - justify-content: end; - } - .xl\:bui-jc-between { - justify-content: space-between; - } - .xl\:bui-fd-row { - flex-direction: row; - } - .xl\:bui-fd-column { - flex-direction: column; - } - .xl\:bui-fd-row-reverse { - flex-direction: row-reverse; - } - .xl\:bui-fd-column-reverse { - flex-direction: column-reverse; - } - } -} +@import '../../../packages/ui/src/css/styles.css'; diff --git a/docs-ui/src/css/theme-spotify.css b/docs-ui/src/css/theme-spotify.css index 64afbb6d75..ec1aa8bb29 100644 --- a/docs-ui/src/css/theme-spotify.css +++ b/docs-ui/src/css/theme-spotify.css @@ -1,233 +1 @@ -@font-face { - font-family: CircularSpTitle; - font-weight: 900; - font-display: swap; - unicode-range: U+0, U+D, U+20-7E, U+A0-17E, U+18F, U+192, U+1A0-1A1, U+1AF-1B0, - U+1B5-1B6, U+1C4-1C6, U+1F1-1F3, U+1FA-1FF, U+218-21B, U+237, U+259, - U+2BB-2BC, U+2C6-2C7, U+2C9, U+2D8-2DD, U+300-301, U+303, U+309, U+323, - U+394, U+3A9, U+3BC, U+3C0, U+1E80-1E85, U+1E8A-1E8B, U+1EA0-1EF9, U+1FD6, - U+2007-2008, U+200B, U+2010-2011, U+2013-2014, U+2018-201A, U+201C-201E, - U+2020-2022, U+2026, U+2030, U+2032-2033, U+2039-203A, U+2042, U+2044, - U+2051, U+2070, U+2074-2079, U+2080-2089, U+20AB-20AC, U+2113, U+2117, - U+2122, U+2126, U+2160-2169, U+216C-216F, U+2190-2193, U+2196-2199, U+21A9, - U+21B0-21B5, U+21C6, U+2202, U+2206, U+220F, U+2211-2212, U+2215, - U+2219-221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+22C5, U+24C5, - U+25A0-25A1, U+25AF, U+25B2-25B3, U+25CA-25CB, U+25CF, U+262E, U+2713, - U+2715, U+2780-2788, U+E000, U+E002, U+F6C3, U+FB00-FB04, U+FEFF, U+FF0C, - U+FF0E, U+FF1A-FF1B, U+FFFF; - src: url(https://encore.scdn.co/fonts/CircularSpTitle-Black-4588c99025b967475c31695b0743dd1a.woff2) - format('woff2'), - url(https://encore.scdn.co/fonts/CircularSpTitle-Black-506746f387a26f25aa3d023b3e501d34.woff) - format('woff'); -} -@font-face { - font-family: CircularSpTitle; - font-weight: 700; - font-display: swap; - unicode-range: U+0, U+D, U+20-7E, U+A0-17E, U+18F, U+192, U+1A0-1A1, U+1AF-1B0, - U+1B5-1B6, U+1C4-1C6, U+1F1-1F3, U+1FA-1FF, U+218-21B, U+237, U+259, - U+2BB-2BC, U+2C6-2C7, U+2C9, U+2D8-2DD, U+300-301, U+303, U+309, U+323, - U+394, U+3A9, U+3BC, U+3C0, U+1E80-1E85, U+1E8A-1E8B, U+1EA0-1EF9, U+1FD6, - U+2007-2008, U+200B, U+2010-2011, U+2013-2014, U+2018-201A, U+201C-201E, - U+2020-2022, U+2026, U+2030, U+2032-2033, U+2039-203A, U+2042, U+2044, - U+2051, U+2070, U+2074-2079, U+2080-2089, U+20AB-20AC, U+2113, U+2117, - U+2122, U+2126, U+2160-2169, U+216C-216F, U+2190-2193, U+2196-2199, U+21A9, - U+21B0-21B5, U+21C6, U+2202, U+2206, U+220F, U+2211-2212, U+2215, - U+2219-221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+22C5, U+24C5, - U+25A0-25A1, U+25AF, U+25B2-25B3, U+25CA-25CB, U+25CF, U+262E, U+2713, - U+2715, U+2780-2788, U+E000, U+E002, U+F6C3, U+FB00-FB04, U+FEFF, U+FF0C, - U+FF0E, U+FF1A-FF1B, U+FFFF; - src: url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-b2586b06a2e1522e9d879d84c2792a58.woff2) - format('woff2'), - url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-1624fb2df28c20d7203d7fb86ce8b481.woff) - format('woff'); -} -@font-face { - font-family: CircularSp; - font-weight: 700; - font-display: swap; - unicode-range: U+0, U+D, U+20-7E, U+A0-17E, U+18F, U+192, U+1A0-1A1, U+1AF-1B0, - U+1B5-1B6, U+1C4-1C6, U+1F1-1F3, U+1FA-1FF, U+218-21B, U+237, U+259, - U+2BB-2BC, U+2C6-2C7, U+2C9, U+2D8-2DD, U+300-301, U+303, U+309, U+323, - U+394, U+3A9, U+3BC, U+3C0, U+1E80-1E85, U+1E8A-1E8B, U+1EA0-1EF9, U+1FD6, - U+2007-2008, U+200B, U+2010-2011, U+2013-2014, U+2018-201A, U+201C-201E, - U+2020-2022, U+2026, U+2030, U+2032-2033, U+2039-203A, U+2042, U+2044, - U+2051, U+2070, U+2074-2079, U+2080-2089, U+20AB-20AC, U+2113, U+2117, - U+2122, U+2126, U+2160-2169, U+216C-216F, U+2190-2193, U+2196-2199, U+21A9, - U+21B0-21B5, U+21C6, U+2202, U+2206, U+220F, U+2211-2212, U+2215, - U+2219-221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+22C5, U+24C5, - U+25A0-25A1, U+25AF, U+25B2-25B3, U+25CA-25CB, U+25CF, U+262E, U+2713, - U+2715, U+2780-2788, U+E000, U+E002, U+F6C3, U+FB00-FB04, U+FEFF, U+FF0C, - U+FF0E, U+FF1A-FF1B, U+FFFF; - src: url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2) - format('woff2'), - url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff) - format('woff'); -} -@font-face { - font-family: CircularSp; - font-weight: 400; - font-display: swap; - unicode-range: U+0, U+D, U+20-7E, U+A0-17E, U+18F, U+192, U+1A0-1A1, U+1AF-1B0, - U+1B5-1B6, U+1C4-1C6, U+1F1-1F3, U+1FA-1FF, U+218-21B, U+237, U+259, - U+2BB-2BC, U+2C6-2C7, U+2C9, U+2D8-2DD, U+300-301, U+303, U+309, U+323, - U+394, U+3A9, U+3BC, U+3C0, U+1E80-1E85, U+1E8A-1E8B, U+1EA0-1EF9, U+1FD6, - U+2007-2008, U+200B, U+2010-2011, U+2013-2014, U+2018-201A, U+201C-201E, - U+2020-2022, U+2026, U+2030, U+2032-2033, U+2039-203A, U+2042, U+2044, - U+2051, U+2070, U+2074-2079, U+2080-2089, U+20AB-20AC, U+2113, U+2117, - U+2122, U+2126, U+2160-2169, U+216C-216F, U+2190-2193, U+2196-2199, U+21A9, - U+21B0-21B5, U+21C6, U+2202, U+2206, U+220F, U+2211-2212, U+2215, - U+2219-221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+22C5, U+24C5, - U+25A0-25A1, U+25AF, U+25B2-25B3, U+25CA-25CB, U+25CF, U+262E, U+2713, - U+2715, U+2780-2788, U+E000, U+E002, U+F6C3, U+FB00-FB04, U+FEFF, U+FF0C, - U+FF0E, U+FF1A-FF1B, U+FFFF; - src: url(https://encore.scdn.co/fonts/CircularSp-Book-a00e99ef9996a3a157fb6b746856d04f.woff2) - format('woff2'), - url(https://encore.scdn.co/fonts/CircularSp-Book-3f73da7d35bd81c706bce7bbb84964de.woff) - format('woff'); -} -@font-face { - font-family: CircularSp; - font-weight: 700; - font-display: swap; - unicode-range: U+0, U+D, U+20-7E, U+A0-17E, U+18F, U+192, U+1A0-1A1, U+1AF-1B0, - U+1B5-1B6, U+1C4-1C6, U+1F1-1F3, U+1FA-1FF, U+218-21B, U+237, U+259, - U+2BB-2BC, U+2C6-2C7, U+2C9, U+2D8-2DD, U+300-301, U+303, U+309, U+323, - U+394, U+3A9, U+3BC, U+3C0, U+1E80-1E85, U+1E8A-1E8B, U+1EA0-1EF9, U+1FD6, - U+2007-2008, U+200B, U+2010-2011, U+2013-2014, U+2018-201A, U+201C-201E, - U+2020-2022, U+2026, U+2030, U+2032-2033, U+2039-203A, U+2042, U+2044, - U+2051, U+2070, U+2074-2079, U+2080-2089, U+20AB-20AC, U+2113, U+2117, - U+2122, U+2126, U+2160-2169, U+216C-216F, U+2190-2193, U+2196-2199, U+21A9, - U+21B0-21B5, U+21C6, U+2202, U+2206, U+220F, U+2211-2212, U+2215, - U+2219-221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+22C5, U+24C5, - U+25A0-25A1, U+25AF, U+25B2-25B3, U+25CA-25CB, U+25CF, U+262E, U+2713, - U+2715, U+2780-2788, U+E000, U+E002, U+F6C3, U+FB00-FB04, U+FEFF, U+FF0C, - U+FF0E, U+FF1A-FF1B, U+FFFF; - src: url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2) - format('woff2'), - url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff) - format('woff'); -} -[data-theme-name='spotify'] { - --bui-font-text: CircularSp, CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl, - CircularSp-Grek, CircularSp-Deva; - --bui-font-title: CircularSpTitle, CircularSp-Arab, CircularSp-Hebr, - CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva; - --bui-font-regular: CircularSp, CircularSp-Arab, CircularSp-Hebr, - CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva; - & .bui-Button { - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-3); - } - & .bui-ButtonIcon { - padding: 0; - } - & .bui-MenuPopup { - box-sizing: border-box; - max-width: 21.25rem; - } - & .bui-MenuSubmenuTrigger, - & .bui-MenuItem { - height: auto; - min-height: 2rem; - } - & .bui-Text { - font-family: var(--bui-font-text); - } - & .bui-Heading { - font-family: var(--bui-font-title); - } - & .bui-TableRow { - border-radius: var(--bui-radius-4); - border: none; - } - & .bui-TableRow:hover td:first-child { - border-top-left-radius: var(--bui-radius-2); - border-bottom-left-radius: var(--bui-radius-2); - } - & .bui-TableRow:hover td:last-child { - border-top-right-radius: var(--bui-radius-2); - border-bottom-right-radius: var(--bui-radius-2); - } - & .bui-TableBody:before, - & .bui-TableBody:after { - line-height: var(--bui-space-1); - content: '‌'; - display: block; - } - & .bui-TableHeader .bui-TableRow { - border-bottom: 1px solid var(--bui-border-2); - } - & .bui-TableHead { - font-size: var(--bui-font-size-2); - color: var(--bui-fg-secondary); - font-weight: var(--bui-font-weight-regular); - } - & .bui-HeaderToolbar { - padding-top: var(--bui-space-2); - padding-inline: var(--bui-space-2); - } - & .bui-HeaderToolbarWrapper { - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-3); - border: none; - } - & .bui-HeaderToolbarControls { - right: calc(var(--bui-space-3) - 1px); - } - & .bui-HeaderTabsWrapper { - margin-top: var(--bui-space-2); - margin-inline: var(--bui-space-2); - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-1); - border: none; - } - & .bui-Input { - border-radius: var(--bui-radius-3); - } - & .bui-Tag { - border-radius: var(--bui-radius-full); - } -} -[data-theme-mode='light'][data-theme-name='spotify'] { - --bui-bg-solid: #1ed760; - --bui-bg-solid-hover: #3be477; - --bui-bg-solid-pressed: #1abc54; - --bui-bg-solid-disabled: #0f6c30; - --bui-fg-primary: var(--bui-black); - --bui-fg-secondary: #757575; - --bui-fg-solid: var(--bui-black); - --bui-fg-solid-disabled: #62ab7c; - --bui-border-2: #d9d9d9; - --bui-border-danger: #f87a7a; - --bui-border-warning: #e36d05; - --bui-border-success: #53db83; - --bui-ring: #0003; - & .bui-HeaderToolbarWrapper, - & .bui-HeaderTabsWrapper { - border: 1px solid var(--bui-border-2); - } -} -[data-theme-mode='dark'][data-theme-name='spotify'] { - --bui-bg-app: var(--bui-black); - --bui-bg-solid: #1ed760; - --bui-bg-solid-hover: #3be477; - --bui-bg-solid-pressed: #1abc54; - --bui-bg-solid-disabled: #0f6c30; - --bui-bg-danger: #3b1219; - --bui-bg-warning: #302008; - --bui-bg-success: #132d21; - --bui-fg-primary: var(--bui-white); - --bui-fg-secondary: #9e9e9e; - --bui-fg-disabled: #575757; - --bui-fg-solid: var(--bui-black); - --bui-fg-solid-disabled: #072f15; - --bui-fg-danger: #e22b2b; - --bui-fg-warning: #e36d05; - --bui-fg-success: #1db954; - --bui-border-2: #373737; - --bui-border-danger: #f87a7a; - --bui-border-warning: #e36d05; - --bui-border-success: #53db83; - --bui-ring: #fff3; -} +@import '../../../.storybook/themes/spotify.css'; diff --git a/docs-ui/src/utils/changelogs/v0.9.0.ts b/docs-ui/src/utils/changelogs/v0.9.0.ts index de5c40c5de..e0a3993d33 100644 --- a/docs-ui/src/utils/changelogs/v0.9.0.ts +++ b/docs-ui/src/utils/changelogs/v0.9.0.ts @@ -114,7 +114,7 @@ After: 'switch', 'skeleton', 'plugin-header', - 'header-page', + 'header', 'tabs', ], version: '0.9.0', diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index a1ef483785..3431563835 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -62,13 +62,17 @@ export const components: Page[] = [ slug: 'plugin-header', }, { - title: 'HeaderPage', - slug: 'header-page', + title: 'Header', + slug: 'header', }, { title: 'Link', slug: 'link', }, + { + title: 'List', + slug: 'list', + }, { title: 'Menu', slug: 'menu', @@ -85,6 +89,10 @@ export const components: Page[] = [ title: 'RadioGroup', slug: 'radio-group', }, + { + title: 'SearchAutocomplete', + slug: 'search-autocomplete', + }, { title: 'SearchField', slug: 'search-field', @@ -138,3 +146,10 @@ export const components: Page[] = [ slug: 'visually-hidden', }, ]; + +export const hooks: Page[] = [ + { + title: 'useBreakpoint', + slug: 'use-breakpoint', + }, +]; diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts index ec762536c6..5f00c5be5d 100644 --- a/docs-ui/src/utils/types.ts +++ b/docs-ui/src/utils/types.ts @@ -15,14 +15,17 @@ export type Component = | 'flex' | 'grid' | 'plugin-header' - | 'header-page' + | 'header' | 'heading' | 'icon' | 'link' + | 'list' + | 'list-row' | 'menu' | 'password-field' | 'radio-group' | 'scrollarea' + | 'search-autocomplete' | 'searchfield' | 'select' | 'skeleton' @@ -35,14 +38,22 @@ export type Component = | 'tooltip' | 'visually-hidden'; +export type Hook = 'use-breakpoint'; + export type Version = `${number}.${number}.${number}`; -export interface ChangelogProps { - components: Component[]; +export type AtLeastOne = K extends string + ? Pick & Partial> + : never; + +export type ChangelogProps = { description: string; version: Version; prs: string[]; breaking?: boolean; commitSha?: string; migration?: string; -} +} & AtLeastOne<{ + components: Component[]; + hooks: Hook[]; +}>; diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock index b974012bec..9df50a64af 100644 --- a/docs-ui/yarn.lock +++ b/docs-ui/yarn.lock @@ -5,14 +5,14 @@ __metadata: version: 8 cacheKey: 10 -"@babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/code-frame@npm:7.28.6" +"@babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" dependencies: "@babel/helper-validator-identifier": "npm:^7.28.5" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.1.1" - checksum: 10/93e7ed9e039e3cb661bdb97c26feebafacc6ec13d745881dae5c7e2708f579475daebe7a3b5d23b183bb940b30744f52f4a5bcb65b4df03b79d82fcb38495784 + checksum: 10/199e15ff89007dd30675655eec52481cb245c9fdf4f81e4dc1f866603b0217b57aff25f5ffa0a95bbc8e31eb861695330cd7869ad52cc211aa63016320ef72c5 languageName: node linkType: hard @@ -336,14 +336,14 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.34.4, @codemirror/view@npm:^6.35.0": - version: 6.39.14 - resolution: "@codemirror/view@npm:6.39.14" + version: 6.39.16 + resolution: "@codemirror/view@npm:6.39.16" dependencies: "@codemirror/state": "npm:^6.5.0" crelt: "npm:^1.0.6" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/956e79758d97e9fc6f6fd9a21575148cb0126712e0a3f78dc03031ebed1a5787eae581eda140f1af6686a5822b3c9c3a67547d2421a16003d45a2c0b6f83ca5e + checksum: 10/199576febda2a91fe7676b8708627ed2e38d7e964ec8258331422fe7c7f89003eee2de7dec828e09c046de005742fd476cae6ceebc7bd994744f771253bfcbf3 languageName: node linkType: hard @@ -393,14 +393,14 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.1": - version: 0.21.1 - resolution: "@eslint/config-array@npm:0.21.1" +"@eslint/config-array@npm:^0.21.2": + version: 0.21.2 + resolution: "@eslint/config-array@npm:0.21.2" dependencies: "@eslint/object-schema": "npm:^2.1.7" debug: "npm:^4.3.1" - minimatch: "npm:^3.1.2" - checksum: 10/6eaa0435972f735ce52d581f355a0b616e50a9b8a73304a7015398096e252798b9b3b968a67b524eefb0fdeacc57c4d960f0ec6432abe1c1e24be815b88c5d18 + minimatch: "npm:^3.1.5" + checksum: 10/148477ba995cf57fc725601916d5a7914aa249112d8bec2c3ac9122e2b2f540e6ef013ff4f6785346a4b565f09b20db127fa6f7322f5ffbdb3f1f8d2078a531c languageName: node linkType: hard @@ -422,27 +422,27 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^3.3.1": - version: 3.3.3 - resolution: "@eslint/eslintrc@npm:3.3.3" +"@eslint/eslintrc@npm:^3.3.5": + version: 3.3.5 + resolution: "@eslint/eslintrc@npm:3.3.5" dependencies: - ajv: "npm:^6.12.4" + ajv: "npm:^6.14.0" debug: "npm:^4.3.2" espree: "npm:^10.0.1" globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" js-yaml: "npm:^4.1.1" - minimatch: "npm:^3.1.2" + minimatch: "npm:^3.1.5" strip-json-comments: "npm:^3.1.1" - checksum: 10/b586a364ff15ce1b68993aefc051ca330b1fece15fb5baf4a708d00113f9a14895cffd84a5f24c5a97bd4b4321130ab2314f90aa462a250f6b859c2da2cba1f3 + checksum: 10/edabb65693d82a88cac3b2cf932a0f825e986b5e0a21ef08782d07e3a61ad87d39db67cfd5aeb146fd5053e5e24e389dbe5649ab22936a71d633c7b32a7e6d86 languageName: node linkType: hard -"@eslint/js@npm:9.39.2": - version: 9.39.2 - resolution: "@eslint/js@npm:9.39.2" - checksum: 10/6b7f676746f3111b5d1b23715319212ab9297868a0fa9980d483c3da8965d5841673aada2d5653e85a3f7156edee0893a7ae7035211b4efdcb2848154bb947f2 +"@eslint/js@npm:9.39.4": + version: 9.39.4 + resolution: "@eslint/js@npm:9.39.4" + checksum: 10/0a7ab4c4108cf2cadf66849ebd20f5957cc53052b88d8807d0b54e489dbf6ffcaf741e144e7f9b187c395499ce2e6ddc565dbfa4f60c6df455cf2b30bcbdc5a3 languageName: node linkType: hard @@ -759,29 +759,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" - dependencies: - string-width: "npm:^5.1.2" - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: "npm:^7.0.1" - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: "npm:^8.1.0" - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 - languageName: node - linkType: hard - -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 - languageName: node - linkType: hard - "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -1074,28 +1051,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^3.0.0": - version: 3.0.0 - resolution: "@npmcli/agent@npm:3.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10/775c9a7eb1f88c195dfb3bce70c31d0fe2a12b28b754e25c08a3edb4bc4816bfedb7ac64ef1e730579d078ca19dacf11630e99f8f3c3e0fd7b23caa5fd6d30a6 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/fs@npm:4.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10/405c4490e1ff11cf299775449a3c254a366a4b1ffc79d87159b0ee7d5558ac9f6a2f8c0735fd6ff3873cef014cb1a44a5f9127cb6a1b2dbc408718cca9365b5a - languageName: node - linkType: hard - "@octokit/auth-token@npm:^6.0.0": version: 6.0.0 resolution: "@octokit/auth-token@npm:6.0.0" @@ -1220,19 +1175,12 @@ __metadata: languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff - languageName: node - linkType: hard - "@remixicon/react@npm:^4.6.0": - version: 4.8.0 - resolution: "@remixicon/react@npm:4.8.0" + version: 4.9.0 + resolution: "@remixicon/react@npm:4.9.0" peerDependencies: react: ">=18.2.0" - checksum: 10/10241f2e07826ce7d595cf9687a35c96cc9cf9eb68a9431d7dfa1b9df479dbef302874d28c0502cee2a536cf34722de7c49ed12d10a2b071e4e42ba4a278c516 + checksum: 10/3d8f1d86b2bb20ab5e44d15f18811e928b0886f7710eb7a1516afb9913ba72e46facec5dfee382825139d800bcbb6704c15d0c760d0f977c12257d4af8db3295 languageName: node linkType: hard @@ -1243,74 +1191,74 @@ __metadata: languageName: node linkType: hard -"@shikijs/core@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/core@npm:3.22.0" +"@shikijs/core@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/core@npm:3.23.0" dependencies: - "@shikijs/types": "npm:3.22.0" + "@shikijs/types": "npm:3.23.0" "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" hast-util-to-html: "npm:^9.0.5" - checksum: 10/fc716bb042accd5061d0de142ee28c20b6e6d489f6516b901ee4db9ed5d802f59c9033106d8140c111a909b761d79523fced92c1367a6f97c4f0e0dad0f29bc9 + checksum: 10/b692850d79d36f90cb92d517de60c3739e888a3ce0b49450090fa954166eae6655ae1884dbbb971e254b907644cc6aa7757fa84fc2437c45208e09143d4f75ab languageName: node linkType: hard -"@shikijs/engine-javascript@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/engine-javascript@npm:3.22.0" +"@shikijs/engine-javascript@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/engine-javascript@npm:3.23.0" dependencies: - "@shikijs/types": "npm:3.22.0" + "@shikijs/types": "npm:3.23.0" "@shikijs/vscode-textmate": "npm:^10.0.2" oniguruma-to-es: "npm:^4.3.4" - checksum: 10/2fcadb897d0220ca70cc63d697e474ce13addc462a387249a852c882a58b9a1e7d6f9b42f694d324403c4acde8486dd54823228602f5ff4270e4298a43c10f51 + checksum: 10/0ca854fb05a14a97fc83c6a54291af6b387fb1c319bd09426eeca3e0ed597bb0b81a79459838c65d0fadce012ba3f166bebb587828d0cc5511b943c743eae2be languageName: node linkType: hard -"@shikijs/engine-oniguruma@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/engine-oniguruma@npm:3.22.0" +"@shikijs/engine-oniguruma@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/engine-oniguruma@npm:3.23.0" dependencies: - "@shikijs/types": "npm:3.22.0" + "@shikijs/types": "npm:3.23.0" "@shikijs/vscode-textmate": "npm:^10.0.2" - checksum: 10/611654868bc2538ab4d7c3770ad2829a623fcf354ce5c09ba8fde29ecc106b5e4a6270de7d970da4524c1ad00d4f7ed3e5afef2be2ff0b94f8bb8541d0775320 + checksum: 10/edd8983be86f6b055793813e80ecf31c3cefdd896f3742797ae03f2cbb9f467ac736c4b152d4a5c42dbd17da17d24ff798a15d37bcf6205d7f8cd8f44e2a11e0 languageName: node linkType: hard -"@shikijs/langs@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/langs@npm:3.22.0" +"@shikijs/langs@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/langs@npm:3.23.0" dependencies: - "@shikijs/types": "npm:3.22.0" - checksum: 10/091cdaeda7763e1170a6a289315a8371f07af541ad4d8fe17a247b45c4a3be82dc9d321a1e4589cdc8464a694692a5c8bfdd1909372b6ec52b0caa53d69e4a1f + "@shikijs/types": "npm:3.23.0" + checksum: 10/115b1afb9eb4eb300eb68b146e442f7e6d196878798c5b9d75dd53a6ef1e1c3b27e325353a10973e3fa47d88630e937b8a3cf3b37b6eef80bf2aec3d51657bb3 languageName: node linkType: hard -"@shikijs/themes@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/themes@npm:3.22.0" +"@shikijs/themes@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/themes@npm:3.23.0" dependencies: - "@shikijs/types": "npm:3.22.0" - checksum: 10/ba60849b4056ea4e243e2c2047382312e504d10f8e1ee09d73041d9de54bce30e28c79168406d8a8e3b7a7ab19d0f9c610e3469679f96145aa975460ac7e1407 + "@shikijs/types": "npm:3.23.0" + checksum: 10/741987380445b788aea6cc1e03492bc06950f1a0461edf45083bd226889c5ba78c7ed1af9724afacab7d6471777f0c0e8871577183dfb2cfbceb255663374835 languageName: node linkType: hard "@shikijs/transformers@npm:^3.13.0": - version: 3.22.0 - resolution: "@shikijs/transformers@npm:3.22.0" + version: 3.23.0 + resolution: "@shikijs/transformers@npm:3.23.0" dependencies: - "@shikijs/core": "npm:3.22.0" - "@shikijs/types": "npm:3.22.0" - checksum: 10/782569a5a8a09dae18402162ad10bba36d8c4e4b6f01c613ccc8151071b06872c9bc32f06bb31147bb58b545b034cd5340d21b1c80c2862cef3983fac6eaef3c + "@shikijs/core": "npm:3.23.0" + "@shikijs/types": "npm:3.23.0" + checksum: 10/d3c86e75668254beda1d1bbd34a00995f684dffd4efdaf60edaa8284ecc9130255c7615c7bca9400a1c50724dd0cd0133469b6e75ffe2033613fd64dcfca5821 languageName: node linkType: hard -"@shikijs/types@npm:3.22.0": - version: 3.22.0 - resolution: "@shikijs/types@npm:3.22.0" +"@shikijs/types@npm:3.23.0": + version: 3.23.0 + resolution: "@shikijs/types@npm:3.23.0" dependencies: "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" - checksum: 10/c5abd9050d4645bbf9f6c9af1a3df31d24d823d21c232e47b171cc68bd4865adab20533503a5c97ffb9e87eae2f88e7a3835d64a0a8aa11a69dbfd45aabd58bf + checksum: 10/18b5703d445d53dd6782a3e2c7332009302c6c046f301d9cd99f1a26b9c8329b3e502b096894bffac61f78835c533827bab2ce78a39666ab87b78f8d7ca11f7b languageName: node linkType: hard @@ -1586,9 +1534,9 @@ __metadata: languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.25.4": - version: 4.25.4 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.4" +"@uiw/codemirror-extensions-basic-setup@npm:4.25.8": + version: 4.25.8 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.8" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -1605,13 +1553,13 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/8fa523af7cb4ea36db5263221e5a34ecbf18e57ee5c57fbb46f7bfd08ff08523b9fe2c5ae376117d98edaa1a904d1a2966123710e0d866a78ec2d1359955773c + checksum: 10/a8d83465f9f3393b6e95d98ae7f3616ad57f819bce64224831d3db19647524538fc013973074a63551afa69daad9a8ab05f2e5c7441db7e30e722495d7e991d3 languageName: node linkType: hard "@uiw/codemirror-themes@npm:^4.23.7": - version: 4.25.4 - resolution: "@uiw/codemirror-themes@npm:4.25.4" + version: 4.25.8 + resolution: "@uiw/codemirror-themes@npm:4.25.8" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.0.0" @@ -1620,19 +1568,19 @@ __metadata: "@codemirror/language": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/aab62e2c16a10dbde4592cef704d386c1a9e3caece4ff0eed85e9d9a5acdeaf3fd8717e2cb6a6795e12e890b8600956effb52d453aacbc618934e7ae8c3e59e3 + checksum: 10/e9983b0f6e663ca200d36437b6b52b4061ce5ccefece6f738b15370a8a7ac6774e7139a82e9e28ae273692e25d0c0804693587ea0967e163a1c7ac8cf3859cd1 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.23.7": - version: 4.25.4 - resolution: "@uiw/react-codemirror@npm:4.25.4" + version: 4.25.8 + resolution: "@uiw/react-codemirror@npm:4.25.8" dependencies: "@babel/runtime": "npm:^7.18.6" "@codemirror/commands": "npm:^6.1.0" "@codemirror/state": "npm:^6.1.1" "@codemirror/theme-one-dark": "npm:^6.0.0" - "@uiw/codemirror-extensions-basic-setup": "npm:4.25.4" + "@uiw/codemirror-extensions-basic-setup": "npm:4.25.8" codemirror: "npm:^6.0.0" peerDependencies: "@babel/runtime": ">=7.11.0" @@ -1642,7 +1590,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/679966cfa5cc2e7d5b7c8b40060018bebfc3a2d66492672c64e659e1c719cedd7a10ed7fb3aa014d77c42927581c4ebdf6760ab56568225781798b35dc13b8c6 + checksum: 10/8c974e22dad1ad6231f33f7db42cd5b68caaf9bea545539b06b8a89dda3427eebadf47c8f48ee0d74cdf5a25000a8fcc02bac9fe560b624955eedf1f9bb47a85 languageName: node linkType: hard @@ -1788,13 +1736,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^3.0.0": - version: 3.0.1 - resolution: "abbrev@npm:3.0.1" - checksum: 10/ebd2c149dda6f543b66ce3779ea612151bb3aa9d0824f169773ee9876f1ca5a4e0adbcccc7eed048c04da7998e1825e2aa76fcca92d9e67dea50ac2b0a58dc2e - languageName: node - linkType: hard - "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -1813,40 +1754,19 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": - version: 7.1.4 - resolution: "agent-base@npm:7.1.4" - checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 - languageName: node - linkType: hard - -"ajv@npm:^6.12.4": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" +"ajv@npm:^6.14.0": + version: 6.14.0 + resolution: "ajv@npm:6.14.0" dependencies: fast-deep-equal: "npm:^3.1.1" fast-json-stable-stringify: "npm:^2.0.0" json-schema-traverse: "npm:^0.4.1" uri-js: "npm:^4.2.2" - checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c + checksum: 10/c71f14dd2b6f2535d043f74019c8169f7aeb1106bafbb741af96f34fdbf932255c919ddd46344043d03b62ea0ccb319f83667ec5eedf612393f29054fe5ce4a5 languageName: node linkType: hard -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 10/2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b - languageName: node - linkType: hard - -"ansi-regex@npm:^6.0.1": - version: 6.1.0 - resolution: "ansi-regex@npm:6.1.0" - checksum: 10/495834a53b0856c02acd40446f7130cb0f8284f4a39afdab20d5dc42b2e198b1196119fe887beed8f9055c4ff2055e3b2f6d4641d0be018cdfb64fedf6fc1aac - languageName: node - linkType: hard - -"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": +"ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" dependencies: @@ -1855,23 +1775,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 - languageName: node - linkType: hard - -"anymatch@npm:~3.1.2": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10/3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 - languageName: node - linkType: hard - "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -2069,13 +1972,6 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.0.0": - version: 2.3.0 - resolution: "binary-extensions@npm:2.3.0" - checksum: 10/bcad01494e8a9283abf18c1b967af65ee79b0c6a9e6fcfafebfe91dbe6e0fc7272bafb73389e198b310516ae04f7ad17d79aacf6cb4c0d5d5202a7e2e52c7d98 - languageName: node - linkType: hard - "brace-expansion@npm:^1.1.7": version: 1.1.12 resolution: "brace-expansion@npm:1.1.12" @@ -2095,7 +1991,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3, braces@npm:~3.0.2": +"braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -2119,26 +2015,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^19.0.1": - version: 19.0.1 - resolution: "cacache@npm:19.0.1" - dependencies: - "@npmcli/fs": "npm:^4.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^10.2.2" - lru-cache: "npm:^10.0.1" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^12.0.0" - tar: "npm:^7.4.3" - unique-filename: "npm:^4.0.0" - checksum: 10/ea026b27b13656330c2bbaa462a88181dcaa0435c1c2e705db89b31d9bdf7126049d6d0445ba746dca21454a0cfdf1d6f47fd39d34c8c8435296b30bc5738a13 - languageName: node - linkType: hard - "call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -2230,32 +2106,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.6.0": - version: 3.6.0 - resolution: "chokidar@npm:3.6.0" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df - languageName: node - linkType: hard - -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c - languageName: node - linkType: hard - "client-only@npm:0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" @@ -2394,7 +2244,16 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0, debug@npm:^4.4.3": +"debug@npm:^3.2.7": + version: 3.2.7 + resolution: "debug@npm:3.2.7" + dependencies: + ms: "npm:^2.1.1" + checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639 + languageName: node + linkType: hard + +"debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -2406,15 +2265,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.2.7": - version: 3.2.7 - resolution: "debug@npm:3.2.7" - dependencies: - ms: "npm:^2.1.1" - checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639 - languageName: node - linkType: hard - "decode-named-character-reference@npm:^1.0.0": version: 1.2.0 resolution: "decode-named-character-reference@npm:1.2.0" @@ -2460,7 +2310,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.3, detect-libc@npm:^2.1.2": +"detect-libc@npm:^2.1.2": version: 2.1.2 resolution: "detect-libc@npm:2.1.2" checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 @@ -2496,15 +2346,15 @@ __metadata: "@types/react-dom": "npm:19.2.3" "@uiw/codemirror-themes": "npm:^4.23.7" "@uiw/react-codemirror": "npm:^4.23.7" - chokidar: "npm:^3.6.0" clsx: "npm:^2.1.1" eslint: "npm:^9" eslint-config-next: "npm:16.1.6" html-react-parser: "npm:^5.2.5" - lightningcss: "npm:^1.28.2" motion: "npm:^12.4.1" next: "npm:16.1.6" next-mdx-remote-client: "npm:^2.1.2" + postcss: "npm:^8.5.6" + postcss-import: "npm:^16.1.1" prop-types: "npm:^15.8.1" react: "npm:19.2.4" react-dom: "npm:19.2.4" @@ -2572,13 +2422,6 @@ __metadata: languageName: node linkType: hard -"eastasianwidth@npm:^0.2.0": - version: 0.2.0 - resolution: "eastasianwidth@npm:0.2.0" - checksum: 10/9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.5.263": version: 1.5.282 resolution: "electron-to-chromium@npm:1.5.282" @@ -2586,13 +2429,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: 10/c72d67a6821be15ec11997877c437491c313d924306b8da5d87d2a2bcc2cec9903cb5b04ee1a088460501d8e5b44f10df82fdc93c444101a7610b80c8b6938e1 - languageName: node - linkType: hard - "emoji-regex@npm:^9.2.2": version: 9.2.2 resolution: "emoji-regex@npm:9.2.2" @@ -2600,15 +2436,6 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.13": - version: 0.1.13 - resolution: "encoding@npm:0.1.13" - dependencies: - iconv-lite: "npm:^0.6.2" - checksum: 10/bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f - languageName: node - linkType: hard - "entities@npm:^4.2.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -2623,20 +2450,6 @@ __metadata: languageName: node linkType: hard -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e - languageName: node - linkType: hard - -"err-code@npm:^2.0.2": - version: 2.0.3 - resolution: "err-code@npm:2.0.3" - checksum: 10/1d20d825cdcce8d811bfbe86340f4755c02655a7feb2f13f8c880566d9d72a3f6c92c192a6867632e490d6da67b678271f46e01044996a6443e870331100dfdd - languageName: node - linkType: hard - "es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": version: 1.24.0 resolution: "es-abstract@npm:1.24.0" @@ -3008,22 +2821,22 @@ __metadata: linkType: hard "eslint@npm:^9": - version: 9.39.2 - resolution: "eslint@npm:9.39.2" + version: 9.39.4 + resolution: "eslint@npm:9.39.4" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-array": "npm:^0.21.2" "@eslint/config-helpers": "npm:^0.4.2" "@eslint/core": "npm:^0.17.0" - "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.39.2" + "@eslint/eslintrc": "npm:^3.3.5" + "@eslint/js": "npm:9.39.4" "@eslint/plugin-kit": "npm:^0.4.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" - ajv: "npm:^6.12.4" + ajv: "npm:^6.14.0" chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" @@ -3042,7 +2855,7 @@ __metadata: is-glob: "npm:^4.0.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" + minimatch: "npm:^3.1.5" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" peerDependencies: @@ -3052,7 +2865,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/53ff0e9c8264e7e8d40d50fdc0c0df0b701cfc5289beedfb686c214e3e7b199702f894bbd1bb48653727bb1ecbd1147cf5f555a4ae71e1daf35020cdc9072d9f + checksum: 10/de95093d710e62e8c7e753220d985587c40f4a05247ed4393ffb6e6cb43a60e825a47fc5b4263151bb2fc5871a206a31d563ccbabdceee1711072ae12db90adf languageName: node linkType: hard @@ -3167,13 +2980,6 @@ __metadata: languageName: node linkType: hard -"exponential-backoff@npm:^3.1.1": - version: 3.1.2 - resolution: "exponential-backoff@npm:3.1.2" - checksum: 10/ca2f01f1aa4dafd3f3917bd531ab5be08c6f5f4b2389d2e974f903de3cbeb50b9633374353516b6afd70905775e33aba11afab1232d3acf0aa2963b98a611c51 - languageName: node - linkType: hard - "extend@npm:^3.0.0": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -3297,21 +3103,11 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0": - version: 3.3.1 - resolution: "foreground-child@npm:3.3.1" +"framer-motion@npm:^12.35.2": + version: 12.35.2 + resolution: "framer-motion@npm:12.35.2" dependencies: - cross-spawn: "npm:^7.0.6" - signal-exit: "npm:^4.0.1" - checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 - languageName: node - linkType: hard - -"framer-motion@npm:^12.33.0": - version: 12.33.0 - resolution: "framer-motion@npm:12.33.0" - dependencies: - motion-dom: "npm:^12.33.0" + motion-dom: "npm:^12.35.2" motion-utils: "npm:^12.29.2" tslib: "npm:^2.4.0" peerDependencies: @@ -3325,35 +3121,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/e3dd7c3167e4ea7d38d9020a672f8ab8b68fbc9994ae3c354e5d8b1c7827747db9206f6bdb024a4dab1b7ebd10ea044a14bacf50b7868271c59b1b5b2579dbb7 - languageName: node - linkType: hard - -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10/af143246cf6884fe26fa281621d45cfe111d34b30535a475bfa38dafe343dadb466c047a924ffc7d6b7b18265df4110224ce3803806dbb07173bf2087b648d7f - languageName: node - linkType: hard - -"fsevents@npm:~2.3.2": - version: 2.3.3 - resolution: "fsevents@npm:2.3.3" - dependencies: - node-gyp: "npm:latest" - checksum: 10/4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0 - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin + checksum: 10/10af699ff1e35a166ef60ceab464479b81624ef74de3ec9e11b427f86bd7bf2c8c8a9f24fb0646288b2d4b0c1b219203da351821fc568c7b91c6821594af4a3f languageName: node linkType: hard @@ -3440,7 +3208,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -3458,22 +3226,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": - version: 10.4.5 - resolution: "glob@npm:10.4.5" - dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" - bin: - glob: dist/esm/bin.mjs - checksum: 10/698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac - languageName: node - linkType: hard - "globals@npm:16.4.0": version: 16.4.0 resolution: "globals@npm:16.4.0" @@ -3505,13 +3257,6 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 - languageName: node - linkType: hard - "has-bigints@npm:^1.0.2": version: 1.1.0 resolution: "has-bigints@npm:1.1.0" @@ -3707,42 +3452,6 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.1": - version: 4.2.0 - resolution: "http-cache-semantics@npm:4.2.0" - checksum: 10/4efd2dfcfeea9d5e88c84af450b9980be8a43c2c8179508b1c57c7b4421c855f3e8efe92fa53e0b3f4a43c85824ada930eabbc306d1b3beab750b6dcc5187693 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^7.0.0": - version: 7.0.2 - resolution: "http-proxy-agent@npm:7.0.2" - dependencies: - agent-base: "npm:^7.1.0" - debug: "npm:^4.3.4" - checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^7.0.1": - version: 7.0.6 - resolution: "https-proxy-agent@npm:7.0.6" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:4" - checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10/24e3292dd3dadaa81d065c6f8c41b274a47098150d444b96e5f53b4638a9a71482921ea6a91a1f59bb71d9796de25e04afd05919fa64c360347ba65d3766f10f - languageName: node - linkType: hard - "ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -3792,13 +3501,6 @@ __metadata: languageName: node linkType: hard -"ip-address@npm:^10.0.1": - version: 10.0.1 - resolution: "ip-address@npm:10.0.1" - checksum: 10/09731acda32cd8e14c46830c137e7e5940f47b36d63ffb87c737331270287d631cf25aa95570907a67d3f919fdb25f4470c404eda21e62f22e0a55927f4dd0fb - languageName: node - linkType: hard - "is-alphabetical@npm:^2.0.0": version: 2.0.1 resolution: "is-alphabetical@npm:2.0.1" @@ -3849,15 +3551,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:~2.1.0": - version: 2.1.0 - resolution: "is-binary-path@npm:2.1.0" - dependencies: - binary-extensions: "npm:^2.0.0" - checksum: 10/078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e - languageName: node - linkType: hard - "is-boolean-object@npm:^1.2.1": version: 1.2.2 resolution: "is-boolean-object@npm:1.2.2" @@ -3884,7 +3577,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -3937,13 +3630,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 10/44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 - languageName: node - linkType: hard - "is-generator-function@npm:^1.0.10": version: 1.1.0 resolution: "is-generator-function@npm:1.1.0" @@ -3956,7 +3642,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -4108,13 +3794,6 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e - languageName: node - linkType: hard - "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -4129,19 +3808,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/96f8786eaab98e4bf5b2a5d6d9588ea46c4d06bbc4f2eb861fdd7b6b182b16f71d8a70e79820f335d52653b16d4843b29dd9cdcf38ae80406756db9199497cf3 - languageName: node - linkType: hard - "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4257,126 +3923,6 @@ __metadata: languageName: node linkType: hard -"lightningcss-android-arm64@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-android-arm64@npm:1.30.2" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-arm64@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-darwin-arm64@npm:1.30.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-x64@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-darwin-x64@npm:1.30.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-freebsd-x64@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-freebsd-x64@npm:1.30.2" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-linux-arm-gnueabihf@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"lightningcss-linux-arm64-gnu@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-linux-arm64-gnu@npm:1.30.2" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-arm64-musl@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-linux-arm64-musl@npm:1.30.2" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-linux-x64-gnu@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-linux-x64-gnu@npm:1.30.2" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-x64-musl@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-linux-x64-musl@npm:1.30.2" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-win32-arm64-msvc@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-win32-arm64-msvc@npm:1.30.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-win32-x64-msvc@npm:1.30.2": - version: 1.30.2 - resolution: "lightningcss-win32-x64-msvc@npm:1.30.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"lightningcss@npm:^1.28.2": - version: 1.30.2 - resolution: "lightningcss@npm:1.30.2" - dependencies: - detect-libc: "npm:^2.0.3" - lightningcss-android-arm64: "npm:1.30.2" - lightningcss-darwin-arm64: "npm:1.30.2" - lightningcss-darwin-x64: "npm:1.30.2" - lightningcss-freebsd-x64: "npm:1.30.2" - lightningcss-linux-arm-gnueabihf: "npm:1.30.2" - lightningcss-linux-arm64-gnu: "npm:1.30.2" - lightningcss-linux-arm64-musl: "npm:1.30.2" - lightningcss-linux-x64-gnu: "npm:1.30.2" - lightningcss-linux-x64-musl: "npm:1.30.2" - lightningcss-win32-arm64-msvc: "npm:1.30.2" - lightningcss-win32-x64-msvc: "npm:1.30.2" - dependenciesMeta: - lightningcss-android-arm64: - optional: true - lightningcss-darwin-arm64: - optional: true - lightningcss-darwin-x64: - optional: true - lightningcss-freebsd-x64: - optional: true - lightningcss-linux-arm-gnueabihf: - optional: true - lightningcss-linux-arm64-gnu: - optional: true - lightningcss-linux-arm64-musl: - optional: true - lightningcss-linux-x64-gnu: - optional: true - lightningcss-linux-x64-musl: - optional: true - lightningcss-win32-arm64-msvc: - optional: true - lightningcss-win32-x64-msvc: - optional: true - checksum: 10/d6cc06d9bac295589a49446e9c45a241dfa16f4f81a7318c26cbc0be3e189003ec0da5d9a0fd9bdffc63a3ce05878cc7329277eaac77a826e8b68c73dc96cfda - languageName: node - linkType: hard - "locate-path@npm:^6.0.0": version: 6.0.0 resolution: "locate-path@npm:6.0.0" @@ -4411,13 +3957,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": - version: 10.4.3 - resolution: "lru-cache@npm:10.4.3" - checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a - languageName: node - linkType: hard - "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -4427,25 +3966,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^14.0.3": - version: 14.0.3 - resolution: "make-fetch-happen@npm:14.0.3" - dependencies: - "@npmcli/agent": "npm:^3.0.0" - cacache: "npm:^19.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^4.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^5.0.0" - promise-retry: "npm:^2.0.1" - ssri: "npm:^12.0.0" - checksum: 10/fce0385840b6d86b735053dfe941edc2dd6468fda80fe74da1eeff10cbd82a75760f406194f2bc2fa85b99545b2bc1f84c08ddf994b21830775ba2d1a87e8bdf - languageName: node - linkType: hard - "markdown-extensions@npm:^2.0.0": version: 2.0.0 resolution: "markdown-extensions@npm:2.0.0" @@ -4955,16 +4475,16 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" +"minimatch@npm:^3.1.2, minimatch@npm:^3.1.5": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f languageName: node linkType: hard -"minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": +"minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -4980,88 +4500,12 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^2.0.1": - version: 2.0.1 - resolution: "minipass-collect@npm:2.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 - languageName: node - linkType: hard - -"minipass-fetch@npm:^4.0.0": - version: 4.0.1 - resolution: "minipass-fetch@npm:4.0.1" - dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^7.0.3" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^3.0.1" - dependenciesMeta: - encoding: - optional: true - checksum: 10/7ddfebdbb87d9866e7b5f7eead5a9e3d9d507992af932a11d275551f60006cf7d9178e66d586dbb910894f3e3458d27c0ddf93c76e94d49d0a54a541ddc1263d - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10/56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10/b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b - languageName: node - linkType: hard - -"minipass-sized@npm:^1.0.3": - version: 1.0.3 - resolution: "minipass-sized@npm:1.0.3" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10/40982d8d836a52b0f37049a0a7e5d0f089637298e6d9b45df9c115d4f0520682a78258905e5c8b180fb41b593b0a82cc1361d2c74b45f7ada66334f84d1ecfdd - languageName: node - linkType: hard - -"minipass@npm:^3.0.0": - version: 3.3.6 - resolution: "minipass@npm:3.3.6" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10/a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 - languageName: node - linkType: hard - -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 - languageName: node - linkType: hard - -"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 - languageName: node - linkType: hard - -"motion-dom@npm:^12.33.0": - version: 12.33.0 - resolution: "motion-dom@npm:12.33.0" +"motion-dom@npm:^12.35.2": + version: 12.35.2 + resolution: "motion-dom@npm:12.35.2" dependencies: motion-utils: "npm:^12.29.2" - checksum: 10/0993bb6380c16fc8c9a2f5a5c4638f3d31bf7680e0683292478d8101489bd86830bb59e8fca8d84ee9b7dadbcfd1f1541de2a47dc7a1294ce34abb00ca5caed5 + checksum: 10/dd009e58b178dd80b123a86199ae78ecd6b2fc6c8e03464b2daf43b4218dfcc36042ec0af8fad2c6c157198f56849f90dc033b58f46478b45fbaeaefcc2710ad languageName: node linkType: hard @@ -5073,10 +4517,10 @@ __metadata: linkType: hard "motion@npm:^12.4.1": - version: 12.33.0 - resolution: "motion@npm:12.33.0" + version: 12.35.2 + resolution: "motion@npm:12.35.2" dependencies: - framer-motion: "npm:^12.33.0" + framer-motion: "npm:^12.35.2" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -5089,7 +4533,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/492e5a552b3c548d88a4597e611cf5c2709b26089500bbebf155ee9ec52aeaf00c35f262b65c924a3419f05da92a97caa4617263781d8f6651cc1ac10b97f548 + checksum: 10/3d99a53816634cbee1b38ed8a9a5d88bafbd29eb3bc02e78fc741c604972b4b88d317cf374bba30a1486f727bb1657ef8826f83e669a3b04fd1ec3ef75bfb62d languageName: node linkType: hard @@ -5100,7 +4544,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.6": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -5125,28 +4569,21 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 - languageName: node - linkType: hard - "next-mdx-remote-client@npm:^2.1.2": - version: 2.1.7 - resolution: "next-mdx-remote-client@npm:2.1.7" + version: 2.1.9 + resolution: "next-mdx-remote-client@npm:2.1.9" dependencies: - "@babel/code-frame": "npm:^7.27.1" + "@babel/code-frame": "npm:^7.29.0" "@mdx-js/mdx": "npm:^3.1.1" "@mdx-js/react": "npm:^3.1.1" - remark-mdx-remove-esm: "npm:^1.2.1" - serialize-error: "npm:^12.0.0" + remark-mdx-remove-esm: "npm:^1.2.3" + serialize-error: "npm:^13.0.1" vfile: "npm:^6.0.3" vfile-matter: "npm:^5.0.1" peerDependencies: - react: ^19.1 - react-dom: ^19.1 - checksum: 10/8aaf6c2f11b0ed398703165c35c0355d1f058521780191684e6fe7e45698effe50ba68b154bae9db84b13e82dff98f380224d588acca6e389aeb73062ecdd31d + react: ">= 19.1.0" + react-dom: ">= 19.1.0" + checksum: 10/cb75c6c162addcb45b6597ef1b4e3b681ab11b74b414722f56d715ed135b10bf3e31cde00a9a3d8154fd700d3a993e653e0335d99514692529aa371e58fff353 languageName: node linkType: hard @@ -5210,26 +4647,6 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:latest": - version: 11.3.0 - resolution: "node-gyp@npm:11.3.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^14.0.3" - nopt: "npm:^8.0.0" - proc-log: "npm:^5.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.4.3" - tinyglobby: "npm:^0.2.12" - which: "npm:^5.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10/e7fb17ba72172d743ee791ea470cba1a579d1c37bcaa67a45d221d07df055baf1367d0684b3c7ee2b9b61f260cea77b2016aaac47027dbc0f43030c90b21527d - languageName: node - linkType: hard - "node-releases@npm:^2.0.27": version: 2.0.27 resolution: "node-releases@npm:2.0.27" @@ -5237,21 +4654,10 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^8.0.0": - version: 8.1.0 - resolution: "nopt@npm:8.1.0" - dependencies: - abbrev: "npm:^3.0.0" - bin: - nopt: bin/nopt.js - checksum: 10/26ab456c51a96f02a9e5aa8d1b80ef3219f2070f3f3528a040e32fb735b1e651e17bdf0f1476988d3a46d498f35c65ed662d122f340d38ce4a7e71dd7b20c4bc - languageName: node - linkType: hard - -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": - version: 3.0.0 - resolution: "normalize-path@npm:3.0.0" - checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 +"non-error@npm:^0.1.0": + version: 0.1.0 + resolution: "non-error@npm:0.1.0" + checksum: 10/c8140db92f2054f1789e2523cb1ac872adbe231642b8ab9c6a2c6295ea33f9ad7093a5adfc54ac07ac8cd86c245ee186ecc918d11c9b9ae3c6fa9b2e0ec7197c languageName: node linkType: hard @@ -5398,20 +4804,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": - version: 7.0.3 - resolution: "p-map@npm:7.0.3" - checksum: 10/2ef48ccfc6dd387253d71bf502604f7893ed62090b2c9d73387f10006c342606b05233da0e4f29388227b61eb5aeface6197e166520c465c234552eeab2fe633 - languageName: node - linkType: hard - -"package-json-from-dist@npm:^1.0.0": - version: 1.0.1 - resolution: "package-json-from-dist@npm:1.0.1" - checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 - languageName: node - linkType: hard - "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -5457,16 +4849,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1": - version: 1.11.1 - resolution: "path-scurry@npm:1.11.1" - dependencies: - lru-cache: "npm:^10.2.0" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10/5e8845c159261adda6f09814d7725683257fcc85a18f329880ab4d7cc1d12830967eae5d5894e453f341710d5484b8fdbbd4d75181b4d6e1eb2f4dc7aeadc434 - languageName: node - linkType: hard - "picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -5474,7 +4856,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc @@ -5488,6 +4870,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^2.3.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -5495,6 +4884,26 @@ __metadata: languageName: node linkType: hard +"postcss-import@npm:^16.1.1": + version: 16.1.1 + resolution: "postcss-import@npm:16.1.1" + dependencies: + postcss-value-parser: "npm:^4.0.0" + read-cache: "npm:^1.0.0" + resolve: "npm:^1.1.7" + peerDependencies: + postcss: ^8.0.0 + checksum: 10/2afac2b6e25f263f45ac2168c5f5e4b2e49e3d44c620338138fe89cf81bd83e6056784a26d54c58611d05dda18bc56069212484ec750bbf6d2e29b623460a7f9 + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.0.0": + version: 4.2.0 + resolution: "postcss-value-parser@npm:4.2.0" + checksum: 10/e4e4486f33b3163a606a6ed94f9c196ab49a37a7a7163abfcd469e5f113210120d70b8dd5e33d64636f41ad52316a3725655421eb9a1094f1bcab1db2f555c62 + languageName: node + linkType: hard + "postcss@npm:8.4.31": version: 8.4.31 resolution: "postcss@npm:8.4.31" @@ -5506,6 +4915,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.6": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -5513,23 +4933,6 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^5.0.0": - version: 5.0.0 - resolution: "proc-log@npm:5.0.0" - checksum: 10/35610bdb0177d3ab5d35f8827a429fb1dc2518d9e639f2151ac9007f01a061c30e0c635a970c9b00c39102216160f6ec54b62377c92fac3b7bfc2ad4b98d195c - languageName: node - linkType: hard - -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" - dependencies: - err-code: "npm:^2.0.2" - retry: "npm:^0.12.0" - checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 - languageName: node - linkType: hard - "prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" @@ -5594,12 +4997,12 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.6.0": - version: 3.6.0 - resolution: "readdirp@npm:3.6.0" +"read-cache@npm:^1.0.0": + version: 1.0.0 + resolution: "read-cache@npm:1.0.0" dependencies: - picomatch: "npm:^2.2.1" - checksum: 10/196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7 + pify: "npm:^2.3.0" + checksum: 10/83a39149d9dfa38f0c482ea0d77b34773c92fef07fe7599cdd914d255b14d0453e0229ef6379d8d27d6947f42d7581635296d0cfa7708f05a9bd8e789d398b31 languageName: node linkType: hard @@ -5719,16 +5122,16 @@ __metadata: languageName: node linkType: hard -"remark-mdx-remove-esm@npm:^1.2.1": - version: 1.2.1 - resolution: "remark-mdx-remove-esm@npm:1.2.1" +"remark-mdx-remove-esm@npm:^1.2.3": + version: 1.2.3 + resolution: "remark-mdx-remove-esm@npm:1.2.3" dependencies: "@types/mdast": "npm:^4.0.4" mdast-util-mdxjs-esm: "npm:^2.0.1" unist-util-remove: "npm:^4.0.0" peerDependencies: unified: ^11 - checksum: 10/092e95ca0649cfe08c47900d9922172b531e1d60b0f90955f51a7282620a1ae53a95dddde3f8d789cdd114a2d515a9372bc72b3e46200ff6f930bd89f0985982 + checksum: 10/3ba2257b376cfe589da851f8df0c45ae78fea8b6090e2825cb009c87f81f4f3738aba682123eb380f5c658cfdd6bb84bf3b6158ed41adaac750728be9e78e672 languageName: node linkType: hard @@ -5788,16 +5191,16 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.22.4": - version: 1.22.10 - resolution: "resolve@npm:1.22.10" +"resolve@npm:^1.1.7, resolve@npm:^1.22.4": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/0a398b44da5c05e6e421d70108822c327675febb880eebe905587628de401854c61d5df02866ff34fc4cb1173a51c9f0e84a94702738df3611a62e2acdc68181 + checksum: 10/e1b2e738884a08de03f97ee71494335eba8c2b0feb1de9ae065e82c48997f349f77a2b10e8817e147cf610bfabc4b1cb7891ee8eaf5bf80d4ad514a34c4fab0a languageName: node linkType: hard @@ -5814,16 +5217,16 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": - version: 1.22.10 - resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/d4d878bfe3702d215ea23e75e0e9caf99468e3db76f5ca100d27ebdc527366fee3877e54bce7d47cc72ca8952fc2782a070d238bfa79a550eeb0082384c3b81a + checksum: 10/fd342cad25e52cd6f4f3d1716e189717f2522bfd6641109fe7aa372f32b5714a296ed7c238ddbe7ebb0c1ddfe0b7f71c9984171024c97cf1b2073e3e40ff71a8 languageName: node linkType: hard @@ -5840,13 +5243,6 @@ __metadata: languageName: node linkType: hard -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 - languageName: node - linkType: hard - "reusify@npm:^1.0.4": version: 1.1.0 resolution: "reusify@npm:1.1.0" @@ -5897,13 +5293,6 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 - languageName: node - linkType: hard - "scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" @@ -5920,7 +5309,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.7.1, semver@npm:^7.7.3": +"semver@npm:^7.7.1, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -5929,12 +5318,13 @@ __metadata: languageName: node linkType: hard -"serialize-error@npm:^12.0.0": - version: 12.0.0 - resolution: "serialize-error@npm:12.0.0" +"serialize-error@npm:^13.0.1": + version: 13.0.1 + resolution: "serialize-error@npm:13.0.1" dependencies: - type-fest: "npm:^4.31.0" - checksum: 10/733ddb9a2bd86515d18ef43f6bcb1f55a9d9b900944420e5606725668e64b752e5da971071d8fe1562961a926796257d2edb9fad828ec90a35cc6c32a75449bb + non-error: "npm:^0.1.0" + type-fest: "npm:^5.4.1" + checksum: 10/a31fd603d2b4f6f33f11acc2cabfa451f2efb2257c5bcbb2ea7fd9b2016da5ea4b4c3a81152481740e54c575ef6e351ddd262b68793c09d9b8fd396ce4a5c52c languageName: node linkType: hard @@ -6076,18 +5466,18 @@ __metadata: linkType: hard "shiki@npm:^3.13.0": - version: 3.22.0 - resolution: "shiki@npm:3.22.0" + version: 3.23.0 + resolution: "shiki@npm:3.23.0" dependencies: - "@shikijs/core": "npm:3.22.0" - "@shikijs/engine-javascript": "npm:3.22.0" - "@shikijs/engine-oniguruma": "npm:3.22.0" - "@shikijs/langs": "npm:3.22.0" - "@shikijs/themes": "npm:3.22.0" - "@shikijs/types": "npm:3.22.0" + "@shikijs/core": "npm:3.23.0" + "@shikijs/engine-javascript": "npm:3.23.0" + "@shikijs/engine-oniguruma": "npm:3.23.0" + "@shikijs/langs": "npm:3.23.0" + "@shikijs/themes": "npm:3.23.0" + "@shikijs/types": "npm:3.23.0" "@shikijs/vscode-textmate": "npm:^10.0.2" "@types/hast": "npm:^3.0.4" - checksum: 10/f9977e75f666253cc679e74b774af2afdfa48d52c631ab1448f47a90cd97f2290bcf50356f5b0d01a282da7f9e9f7e1938d585b213688e0d842fcba4a785f870 + checksum: 10/4c2751cac9dbcd61b6c80aed6c97ea4954b083c0cdf0d29fab71dbf90042c82b29f104c2843b24ea593e6d56608973b7c153dd3c05520ae001bc2f294651e398 languageName: node linkType: hard @@ -6139,42 +5529,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: 10/927484aa0b1640fd9473cee3e0a0bcad6fce93fd7bbc18bac9ad0c33686f5d2e2c422fba24b5899c184524af01e11dd2bd051c2bf2b07e47aff8ca72cbfc60d2 - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.5 - resolution: "socks-proxy-agent@npm:8.0.5" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:^4.3.4" - socks: "npm:^2.8.3" - checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c - languageName: node - linkType: hard - -"socks@npm:^2.8.3": - version: 2.8.7 - resolution: "socks@npm:2.8.7" - dependencies: - ip-address: "npm:^10.0.1" - smart-buffer: "npm:^4.2.0" - checksum: 10/d19366c95908c19db154f329bbe94c2317d315dc933a7c2b5101e73f32a555c84fb199b62174e1490082a593a4933d8d5a9b297bde7d1419c14a11a965f51356 - languageName: node - linkType: hard - -"source-map-js@npm:^1.0.2": +"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 @@ -6195,15 +5550,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^12.0.0": - version: 12.0.0 - resolution: "ssri@npm:12.0.0" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10/7024c1a6e39b3f18aa8f1c8290e884fe91b0f9ca5a6c6d410544daad54de0ba664db879afe16412e187c6c292fd60b937f047ee44292e5c2af2dcc6d8e1a9b48 - languageName: node - linkType: hard - "stable-hash@npm:^0.0.5": version: 0.0.5 resolution: "stable-hash@npm:0.0.5" @@ -6221,28 +5567,6 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: "npm:^8.0.0" - is-fullwidth-code-point: "npm:^3.0.0" - strip-ansi: "npm:^6.0.1" - checksum: 10/e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - -"string-width@npm:^5.0.1, string-width@npm:^5.1.2": - version: 5.1.2 - resolution: "string-width@npm:5.1.2" - dependencies: - eastasianwidth: "npm:^0.2.0" - emoji-regex: "npm:^9.2.2" - strip-ansi: "npm:^7.0.1" - checksum: 10/7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 - languageName: node - linkType: hard - "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -6333,24 +5657,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: "npm:^5.0.1" - checksum: 10/ae3b5436d34fadeb6096367626ce987057713c566e1e7768818797e00ac5d62023d0f198c4e681eae9e20701721980b26a64a8f5b91238869592a9c6800719a2 - languageName: node - linkType: hard - -"strip-ansi@npm:^7.0.1": - version: 7.1.0 - resolution: "strip-ansi@npm:7.1.0" - dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 - languageName: node - linkType: hard - "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -6429,20 +5735,14 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.3": - version: 7.5.7 - resolution: "tar@npm:7.5.7" - dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" - yallist: "npm:^5.0.0" - checksum: 10/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21 +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: 10/e37653df3e495daa7ea7790cb161b810b00075bba2e4d6c93fb06a709e747e3ae9da11a120d0489833203926511b39e038a2affbd9d279cfb7a2f3fcccd30b5d languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -6512,10 +5812,12 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.31.0": - version: 4.41.0 - resolution: "type-fest@npm:4.41.0" - checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 +"type-fest@npm:^5.4.1": + version: 5.4.4 + resolution: "type-fest@npm:5.4.4" + dependencies: + tagged-tag: "npm:^1.0.0" + checksum: 10/0bbdca645f95740587f389a2d712fe8d5e9ab7d13e74aac97cf396112510abcaab6b75fd90d65172bc13b02fdfc827e6a871322cc9c1c1a5a2754d9ab264c6f5 languageName: node linkType: hard @@ -6641,24 +5943,6 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-filename@npm:4.0.0" - dependencies: - unique-slug: "npm:^5.0.0" - checksum: 10/6a62094fcac286b9ec39edbd1f8f64ff92383baa430af303dfed1ffda5e47a08a6b316408554abfddd9730c78b6106bef4ca4d02c1231a735ddd56ced77573df - languageName: node - linkType: hard - -"unique-slug@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-slug@npm:5.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10/beafdf3d6f44990e0a5ce560f8f881b4ee811be70b6ba0db25298c31c8cf525ed963572b48cd03be1c1349084f9e339be4241666d7cf1ebdad20598d3c652b27 - languageName: node - linkType: hard - "unist-util-is@npm:^6.0.0": version: 6.0.0 resolution: "unist-util-is@npm:6.0.0" @@ -6942,17 +6226,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^5.0.0": - version: 5.0.0 - resolution: "which@npm:5.0.0" - dependencies: - isexe: "npm:^3.1.1" - bin: - node-which: bin/which.js - checksum: 10/6ec99e89ba32c7e748b8a3144e64bfc74aa63e2b2eacbb61a0060ad0b961eb1a632b08fb1de067ed59b002cec3e21de18299216ebf2325ef0f78e0f121e14e90 - languageName: node - linkType: hard - "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -6960,28 +6233,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10/cebdaeca3a6880da410f75209e68cd05428580de5ad24535f22696d7d9cab134d1f8498599f344c3cf0fb37c1715807a183778d8c648d6cc0cb5ff2bb4236540 - languageName: node - linkType: hard - -"wrap-ansi@npm:^8.1.0": - version: 8.1.0 - resolution: "wrap-ansi@npm:8.1.0" - dependencies: - ansi-styles: "npm:^6.1.0" - string-width: "npm:^5.0.1" - strip-ansi: "npm:^7.0.1" - checksum: 10/7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf - languageName: node - linkType: hard - "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -6989,20 +6240,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10/4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd - languageName: node - linkType: hard - -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10/1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a - languageName: node - linkType: hard - "yaml@npm:^2.0.0": version: 2.8.1 resolution: "yaml@npm:2.8.1" diff --git a/docs/ai/mcp-actions.md b/docs/ai/mcp-actions.md new file mode 100644 index 0000000000..498cd42df2 --- /dev/null +++ b/docs/ai/mcp-actions.md @@ -0,0 +1,256 @@ +--- +id: mcp-actions +title: MCP Actions Backend +description: The MCP Actions Backend exposes actions registered with the Actions Registry as MCP tools. +--- + +The MCP Actions Backend exposes [Actions](../backend-system/core-services/actions.md) registered with the [Actions Registry](../backend-system/core-services/actions-registry.md) as MCP tools. + +## Installation + +This plugin is installed via the `@backstage/plugin-mcp-actions-backend` package. To add it to your backend package, run the following command: + +```bash title="From your root directory" +yarn --cwd packages/backend add @backstage/plugin-mcp-actions-backend +``` + +Then, add the plugin to your backend: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-mcp-actions-backend')); +// ... +backend.start(); +``` + +## Actions Configuration + +Populate the `pluginSources` configuration with the list of plugins you want exposed as MCP tools like so: + +```yaml +backend: + actions: + pluginSources: + - 'catalog' + - 'my-custom-plugin' +``` + +For details on filtering actions, see the [filtering actions documentation](../backend-system/core-services/actions.md#filtering-actions). + +## Single MCP Server Name & Description + +You can configure the name and description of your Backstage MCP server with the following config: + +```yaml title="app-config.yaml" +mcpActions: + name: 'My MCP Server' # defaults to "backstage" + description: 'Tools for interacting with My MCP Server' # optional +``` + +## Namespaced Tool Names + +By default, MCP tool names include the plugin ID prefix to avoid collisions across plugins. For example, an action registered as `greet-user` by `my-custom-plugin` is exposed as `my-custom-plugin.greet-user`. + +You can disable this if you need the short names for backward compatibility: + +```yaml title="app-config.yaml" +mcpActions: + namespacedToolNames: false +``` + +## Multiple MCP Servers + +By default, the plugin serves a single MCP server at `/api/mcp-actions/v1` that exposes all available actions. You can split actions into multiple focused servers by configuring `mcpActions.servers`, where each key becomes a separate MCP server endpoint. + +```yaml title="app-config.yaml" +mcpActions: + servers: + catalog: + name: 'Backstage Catalog' + description: 'Tools for interacting with the software catalog' + filter: + include: + - id: 'catalog:*' + scaffolder: + name: 'Backstage Scaffolder' + description: 'Tools for creating new software from templates' + filter: + include: + - id: 'scaffolder:*' +``` + +This creates two MCP server endpoints: + +- `http://localhost:7007/api/mcp-actions/v1/catalog` +- `http://localhost:7007/api/mcp-actions/v1/scaffolder` + +Each server uses include filter rules with glob patterns on action IDs to control which actions are exposed. For example, `id: 'catalog:*'` matches all actions registered by the catalog plugin. + +When `mcpActions.servers` is not configured, the plugin behaves exactly as before with a single server at `/api/mcp-actions/v1`. + +### Filter Rules + +Include and exclude filter rules support glob patterns on action IDs and attribute matching. Exclude rules take precedence over include rules. When include rules are specified, actions must match at least one include rule to be exposed. + +```yaml title="app-config.yaml" +mcpActions: + servers: + catalog: + name: 'Backstage Catalog' + filter: + include: + - id: 'catalog:*' + exclude: + - attributes: + destructive: true +``` + +## Authentication Configuration + +By default, the Backstage backend requires authentication for all requests. + +### External Access with Static Tokens + +:::warning +This is meant to be a temporary workaround until device authentication is completed. +::: + +Configure external access with static tokens in your app configuration: + +```yaml title="app-config.yaml" +backend: + auth: + externalAccess: + - type: static + options: + token: ${MCP_TOKEN} + subject: mcp-clients + accessRestrictions: + - plugin: mcp-actions + - plugin: catalog +``` + +Generate a secure token: + +```bash +node -p 'require("crypto").randomBytes(24).toString("base64")' +``` + +Set the `MCP_TOKEN` environment variable and configure your MCP client to send: + +```http +Authorization: Bearer +``` + +For more details about external access tokens and service-to-service authentication, see the +[Service-to-Service Auth documentation](../auth/service-to-service-auth.md). + +### Experimental: Dynamic Client Registration + +:::warning +This feature is highly experimental and only works with the New Frontend System. Proceed with caution. +::: + +You can configure the auth-backend and install the auth frontend plugin to enable **Dynamic Client Registration** with MCP clients. This means you do not need to manually configure a token in your MCP client settings. Instead, a client can request a token on your behalf. When adding the MCP server to an MCP client like Cursor or Claude, a popup requiring your approval will open in your Backstage instance (powered by the auth plugin). + +**Requirements:** + +- The `@backstage/plugin-auth-backend` plugin must be configured. +- The new `@backstage/plugin-auth` frontend plugin must be configured. + +**Installation:** + +1. Install the `@backstage/plugin-auth` frontend plugin: + + ```bash + yarn --cwd packages/app add @backstage/plugin-auth + ``` + +2. If you use [feature discovery](../frontend-system/architecture/10-app.md#feature-discovery) the plugin will be added automatically, if you prefer explicit registration, register the plugin as a feature like this: + + ```tsx title="packages/app/src/App.tsx" + import authPlugin from '@backstage/plugin-auth'; + + const app = createApp({ + features: [ + // ...other features + authPlugin, + ], + }); + ``` + +3. Enable the feature: + + ```yaml title="app-config.yaml" + auth: + experimentalDynamicClientRegistration: + enabled: true + + # Optional: limit valid callback URLs for added security + allowedRedirectUriPatterns: + - cursor://* + ``` + +## Configuring MCP Clients + +The MCP server supports both **Server-Sent Events (SSE)** and **Streamable HTTP** protocols. + +:::warning +The SSE protocol is deprecated and will be removed in a future release. +::: + +### Endpoints + +- **Streamable HTTP:** `http://localhost:7007/api/mcp-actions/v1` +- **SSE (deprecated):** `http://localhost:7007/api/mcp-actions/v1/sse` + +```json +{ + "mcpServers": { + "backstage-actions": { + "url": "http://localhost:7007/api/mcp-actions/v1", + "headers": { + "Authorization": "Bearer ${MCP_TOKEN}" + } + } + } +} +``` + +The `${MCP_TOKEN}` environment variable would be an [external access static token](#external-access-with-static-tokens). + +### Multiple Servers + +When `mcpActions.servers` is configured, each server key becomes part of the URL. For example, with servers named `catalog` and `scaffolder`: + +- `http://localhost:7007/api/mcp-actions/v1/catalog` +- `http://localhost:7007/api/mcp-actions/v1/scaffolder` + +```json +{ + "mcpServers": { + "backstage-catalog": { + "url": "http://localhost:7007/api/mcp-actions/v1/catalog", + "headers": { + "Authorization": "Bearer ${MCP_TOKEN}" + } + }, + "backstage-scaffolder": { + "url": "http://localhost:7007/api/mcp-actions/v1/scaffolder", + "headers": { + "Authorization": "Bearer ${MCP_TOKEN}" + } + } + } +} +``` + +## Metrics + +The MCP Actions Backend emits metrics for the following operations: + +- `mcp.server.operation.duration`: The duration taken to process an individual MCP operation +- `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server + +See the [OpenTelemetry tutorial](../tutorials/setup-opentelemetry.md) to learn how to make these metrics available. diff --git a/docs/ai/well-known-actions.md b/docs/ai/well-known-actions.md new file mode 100644 index 0000000000..d6de8407cf --- /dev/null +++ b/docs/ai/well-known-actions.md @@ -0,0 +1,30 @@ +--- +id: well-known-actions +title: Well-known Actions +description: This section lists a number of well-known actions that are part of the Actions Registry. +--- + +This section lists a number of well-known [Actions](../backend-system/core-services/actions.md) registered with the [Actions Registry](../backend-system/core-services/actions-registry.md). + +## Actions + +This is a (non-exhaustive) list of actions that are known to be part of the Actions Registry. Entries are in the format: "`action-name` (Action Title): Shortened Action Description" + +### Auth + +- `auth.who-am-i` (Who Am I): Returns the catalog entity and user info for the currently authenticated user. This action requires user credentials and cannot be used with service or unauthenticated credentials. + +### Catalog + +- `catalog.get-catalog-entity` (Get Catalog Entity): This allows you to get a single entity from the software catalog. +- `catalog.query-catalog-entities` (Query Catalog Entities): Query entities from the Backstage Software Catalog using predicate filters. +- `catalog.register-entity` (Register entity in the Catalog): Registers one or more entities in the Backstage catalog by creating a Location entity that points to a remote `catalog-info.yaml` file. +- `catalog.unregister-entity` (Unregister entity from the Catalog): Unregisters a Location entity and all entities it owns from the Backstage catalog. +- `catalog.validate-entity` (Validate Catalog Entity): This action can be used to validate `catalog-info.yaml` file contents meant to be used with the software catalog. + +### Scaffolder + +- `scaffolder.dry-run-template` (Dry Run Scaffolder Template): Dry-runs a scaffolder template to validate it without making changes. Returns success with execution logs, or errors for validation failures. +- `scaffolder.list-scaffolder-actions` (List Scaffolder Actions): Lists all installed Scaffolder actions. +- `scaffolder.list-scaffolder-tasks` (List Scaffolder Tasks): This allows you to list scaffolder tasks that have been created. +- `scaffolder.get-scaffolder-task-logs` (Get Scaffolder Task Logs): This allows you to fetch the logs of a given scaffolder task. diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md index 84c0300543..136d70778e 100644 --- a/docs/api/deprecations.md +++ b/docs/api/deprecations.md @@ -83,7 +83,7 @@ migrate to your own custom API. First, you'll need to define a new Utility API reference. If you're only using the API for sign-in, you can put the definition in `packages/app/src/apis.ts`. -However, if you need to access your auth API inside plugins you you'll need to +However, if you need to access your auth API inside plugins you'll need to export it from a common package. If you don't already have one, we recommend creating `@internal/apis` and from there exporting the API reference. diff --git a/docs/assets/uiguide/backstage-website-registered-catalog-view.png b/docs/assets/uiguide/backstage-website-registered-catalog-view.png new file mode 100644 index 0000000000..9263eff0c7 Binary files /dev/null and b/docs/assets/uiguide/backstage-website-registered-catalog-view.png differ diff --git a/docs/assets/uiguide/catalog-filter-options.png b/docs/assets/uiguide/catalog-filter-options.png new file mode 100644 index 0000000000..f2a85ab192 Binary files /dev/null and b/docs/assets/uiguide/catalog-filter-options.png differ diff --git a/docs/assets/uiguide/catalog-graph.png b/docs/assets/uiguide/catalog-graph.png new file mode 100644 index 0000000000..59e290b0f0 Binary files /dev/null and b/docs/assets/uiguide/catalog-graph.png differ diff --git a/docs/assets/uiguide/component-name-updated.png b/docs/assets/uiguide/component-name-updated.png new file mode 100644 index 0000000000..602e2e551a Binary files /dev/null and b/docs/assets/uiguide/component-name-updated.png differ diff --git a/docs/assets/uiguide/confirm-unregister-entity.png b/docs/assets/uiguide/confirm-unregister-entity.png new file mode 100644 index 0000000000..89a565ff35 Binary files /dev/null and b/docs/assets/uiguide/confirm-unregister-entity.png differ diff --git a/docs/assets/uiguide/curve-monotone.png b/docs/assets/uiguide/curve-monotone.png new file mode 100644 index 0000000000..52275ec780 Binary files /dev/null and b/docs/assets/uiguide/curve-monotone.png differ diff --git a/docs/assets/uiguide/curve-step-before.png b/docs/assets/uiguide/curve-step-before.png new file mode 100644 index 0000000000..a9aa4c8e6f Binary files /dev/null and b/docs/assets/uiguide/curve-step-before.png differ diff --git a/docs/assets/uiguide/details-for-registered-backstage-website.png b/docs/assets/uiguide/details-for-registered-backstage-website.png new file mode 100644 index 0000000000..00d55026f2 Binary files /dev/null and b/docs/assets/uiguide/details-for-registered-backstage-website.png differ diff --git a/docs/assets/uiguide/details-system-entity.png b/docs/assets/uiguide/details-system-entity.png new file mode 100644 index 0000000000..c667421603 Binary files /dev/null and b/docs/assets/uiguide/details-system-entity.png differ diff --git a/docs/assets/uiguide/enter-name-of-new-component.png b/docs/assets/uiguide/enter-name-of-new-component.png new file mode 100644 index 0000000000..fc6a11a360 Binary files /dev/null and b/docs/assets/uiguide/enter-name-of-new-component.png differ diff --git a/docs/assets/uiguide/enter-url-of-component.png b/docs/assets/uiguide/enter-url-of-component.png new file mode 100644 index 0000000000..29fecb7bfd Binary files /dev/null and b/docs/assets/uiguide/enter-url-of-component.png differ diff --git a/docs/assets/uiguide/entities-owned-by-me.png b/docs/assets/uiguide/entities-owned-by-me.png new file mode 100644 index 0000000000..e0dda5e680 Binary files /dev/null and b/docs/assets/uiguide/entities-owned-by-me.png differ diff --git a/docs/assets/uiguide/entity-actions.png b/docs/assets/uiguide/entity-actions.png new file mode 100644 index 0000000000..3c725758e4 Binary files /dev/null and b/docs/assets/uiguide/entity-actions.png differ diff --git a/docs/assets/uiguide/error-creating-new-component.png b/docs/assets/uiguide/error-creating-new-component.png new file mode 100644 index 0000000000..8c7da95159 Binary files /dev/null and b/docs/assets/uiguide/error-creating-new-component.png differ diff --git a/docs/assets/uiguide/error-message-catalog-info-file-deleted.png b/docs/assets/uiguide/error-message-catalog-info-file-deleted.png new file mode 100644 index 0000000000..7526ba65aa Binary files /dev/null and b/docs/assets/uiguide/error-message-catalog-info-file-deleted.png differ diff --git a/docs/assets/uiguide/example-website-relationships.png b/docs/assets/uiguide/example-website-relationships.png new file mode 100644 index 0000000000..b4fa6e33c7 Binary files /dev/null and b/docs/assets/uiguide/example-website-relationships.png differ diff --git a/docs/assets/uiguide/filter-by-name.png b/docs/assets/uiguide/filter-by-name.png new file mode 100644 index 0000000000..ca0336eadc Binary files /dev/null and b/docs/assets/uiguide/filter-by-name.png differ diff --git a/docs/assets/uiguide/max-depth-1.png b/docs/assets/uiguide/max-depth-1.png new file mode 100644 index 0000000000..d353b8ae23 Binary files /dev/null and b/docs/assets/uiguide/max-depth-1.png differ diff --git a/docs/assets/uiguide/max-depth-infinite.png b/docs/assets/uiguide/max-depth-infinite.png new file mode 100644 index 0000000000..0f72cccb84 Binary files /dev/null and b/docs/assets/uiguide/max-depth-infinite.png differ diff --git a/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png b/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png new file mode 100644 index 0000000000..51ee73c524 Binary files /dev/null and b/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png differ diff --git a/docs/assets/uiguide/portal-with-annotations.png b/docs/assets/uiguide/portal-with-annotations.png new file mode 100644 index 0000000000..a8c9bc535b Binary files /dev/null and b/docs/assets/uiguide/portal-with-annotations.png differ diff --git a/docs/assets/uiguide/review-select-import.png b/docs/assets/uiguide/review-select-import.png new file mode 100644 index 0000000000..bd4a0acd7c Binary files /dev/null and b/docs/assets/uiguide/review-select-import.png differ diff --git a/docs/assets/uiguide/select-advanced-options.png b/docs/assets/uiguide/select-advanced-options.png new file mode 100644 index 0000000000..27fe7c7b16 Binary files /dev/null and b/docs/assets/uiguide/select-advanced-options.png differ diff --git a/docs/assets/uiguide/select-create-for-new-component.png b/docs/assets/uiguide/select-create-for-new-component.png new file mode 100644 index 0000000000..9fbc612ac0 Binary files /dev/null and b/docs/assets/uiguide/select-create-for-new-component.png differ diff --git a/docs/assets/uiguide/select-create-to-make-component.png b/docs/assets/uiguide/select-create-to-make-component.png new file mode 100644 index 0000000000..e86e9398ca Binary files /dev/null and b/docs/assets/uiguide/select-create-to-make-component.png differ diff --git a/docs/assets/uiguide/select-delete-entity.png b/docs/assets/uiguide/select-delete-entity.png new file mode 100644 index 0000000000..6874c5e83d Binary files /dev/null and b/docs/assets/uiguide/select-delete-entity.png differ diff --git a/docs/assets/uiguide/select-edit-icon-for-component.png b/docs/assets/uiguide/select-edit-icon-for-component.png new file mode 100644 index 0000000000..04d693259d Binary files /dev/null and b/docs/assets/uiguide/select-edit-icon-for-component.png differ diff --git a/docs/assets/uiguide/select-example-website.png b/docs/assets/uiguide/select-example-website.png new file mode 100644 index 0000000000..f8f72699eb Binary files /dev/null and b/docs/assets/uiguide/select-example-website.png differ diff --git a/docs/assets/uiguide/select-register-existing-component.png b/docs/assets/uiguide/select-register-existing-component.png new file mode 100644 index 0000000000..8d3f5c7af4 Binary files /dev/null and b/docs/assets/uiguide/select-register-existing-component.png differ diff --git a/docs/assets/uiguide/select-review-create-component.png b/docs/assets/uiguide/select-review-create-component.png new file mode 100644 index 0000000000..5acce27ec6 Binary files /dev/null and b/docs/assets/uiguide/select-review-create-component.png differ diff --git a/docs/assets/uiguide/select-unregister-entity-from-three-dots.png b/docs/assets/uiguide/select-unregister-entity-from-three-dots.png new file mode 100644 index 0000000000..959ec3f544 Binary files /dev/null and b/docs/assets/uiguide/select-unregister-entity-from-three-dots.png differ diff --git a/docs/assets/uiguide/select-view-graph.png b/docs/assets/uiguide/select-view-graph.png new file mode 100644 index 0000000000..79886a82b4 Binary files /dev/null and b/docs/assets/uiguide/select-view-graph.png differ diff --git a/docs/assets/uiguide/setup-nodejs-cat-owner.png b/docs/assets/uiguide/setup-nodejs-cat-owner.png new file mode 100644 index 0000000000..5e8effec72 Binary files /dev/null and b/docs/assets/uiguide/setup-nodejs-cat-owner.png differ diff --git a/docs/assets/uiguide/simplify-off-merge-relations-off.png b/docs/assets/uiguide/simplify-off-merge-relations-off.png new file mode 100644 index 0000000000..d999a7a5c7 Binary files /dev/null and b/docs/assets/uiguide/simplify-off-merge-relations-off.png differ diff --git a/docs/assets/uiguide/simplify-off-merge-relations-on.png b/docs/assets/uiguide/simplify-off-merge-relations-on.png new file mode 100644 index 0000000000..1003f2dd3d Binary files /dev/null and b/docs/assets/uiguide/simplify-off-merge-relations-on.png differ diff --git a/docs/assets/uiguide/simplify-on-merge-relations-off.png b/docs/assets/uiguide/simplify-on-merge-relations-off.png new file mode 100644 index 0000000000..60cbe0107e Binary files /dev/null and b/docs/assets/uiguide/simplify-on-merge-relations-off.png differ diff --git a/docs/assets/uiguide/simplify-on-merge-relations-on.png b/docs/assets/uiguide/simplify-on-merge-relations-on.png new file mode 100644 index 0000000000..d72489b50e Binary files /dev/null and b/docs/assets/uiguide/simplify-on-merge-relations-on.png differ diff --git a/docs/assets/uiguide/successful-create-new-component.png b/docs/assets/uiguide/successful-create-new-component.png new file mode 100644 index 0000000000..658189b7d2 Binary files /dev/null and b/docs/assets/uiguide/successful-create-new-component.png differ diff --git a/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png b/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png new file mode 100644 index 0000000000..55d3d24baf Binary files /dev/null and b/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png differ diff --git a/docs/assets/uiguide/tutorial-component-open-in-catalog.png b/docs/assets/uiguide/tutorial-component-open-in-catalog.png new file mode 100644 index 0000000000..107974c437 Binary files /dev/null and b/docs/assets/uiguide/tutorial-component-open-in-catalog.png differ diff --git a/docs/assets/uiguide/updated-component-name-in-ui.png b/docs/assets/uiguide/updated-component-name-in-ui.png new file mode 100644 index 0000000000..7b5a9e5648 Binary files /dev/null and b/docs/assets/uiguide/updated-component-name-in-ui.png differ diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index b836bf01f6..1a560d4f62 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -5,13 +5,6 @@ sidebar_label: Auth0 description: Adding Auth0 as an authentication provider in Backstage --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage -[version 1.24](../../releases/v1.24.0.md). If you are still on the old backend -system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/auth0/provider--old.md) -instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Backstage `core-plugin-api` package comes with an Auth0 authentication provider that can authenticate users using OAuth. diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index 89b8be3ff6..0beaa08f18 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -5,13 +5,6 @@ sidebar_label: Bitbucket Server description: Adding Bitbucket Server OAuth as an authentication provider in Backstage --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage -[version 1.24](../../releases/v1.24.0.md). If you are still on the old backend -system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/bitbucketServer/provider--old.md) -instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index 5072aabe50..f2ebfff8d6 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -19,10 +19,6 @@ This provider should only ever be enabled for `development`. To prevent unauthor ### Backend -:::note -This will only work with the new backend system. There is no support for this in the old backend. -::: - Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. ```sh title="From your Backstage root directory" diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 0d4b50dcbe..26b48dec69 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -4,13 +4,6 @@ title: Sign-in Identities and Resolvers description: An introduction to Backstage user identities and sign-in resolvers --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage -[version 1.24](../releases/v1.24.0.md). If you are still on the old backend -system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/identity-resolver--old.md) -instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - By default, every Backstage auth provider is configured only for the use-case of access delegation. This enables Backstage to request resources and actions from external systems on behalf of the user, for example re-triggering a build in CI. diff --git a/docs/auth/index.md b/docs/auth/index.md index fb85eb999b..389f824be3 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -12,7 +12,7 @@ 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). ::: @@ -461,7 +461,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 diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index a55719e564..194eb08d05 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -4,13 +4,6 @@ title: OIDC provider from scratch description: This section shows how to enable and use the Backstage OIDC provider. --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage -[version 1.24](../releases/v1.24.0.md). If you are still on the old backend -system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/oidc--old.md) -instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - This section shows how to enable and use the Backstage OIDC provider. ## Summary diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 1423fa92f7..6655c76b29 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -4,13 +4,6 @@ title: Service to Service Auth description: This section describes service to service authentication works, both internally within Backstage plugins and when external callers want to make requests. --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage -[version 1.24](../releases/v1.24.0.md). If you are still on the old backend -system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/auth/service-to-service-auth--old.md) -instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - This article describes how _service-to-service auth_ works in Backstage, both between Backstage backend plugins and for external callers who want to make requests to them. This is in contrast to _user and user-to-service auth_ which @@ -20,7 +13,7 @@ Each section describes one distinct type of auth flow. ## Standard Plugin-to-Plugin Auth -Backstage plugins that use the new backend system and handle credentials using +Backstage plugins that use the backend system and handle credentials using the `auth` and `httpAuth` service APIs are secure by default, without requiring any configuration. They generate self-signed tokens automatically for making requests to other Backstage backend plugins, and the receivers use the caller's @@ -228,96 +221,6 @@ calling Backstage plugins: Authorization: Bearer eyJhbG... ``` -## Legacy Tokens - -Plugins and backends that are _not_ on the new backend system use a legacy token -flow, where shared static secrets in your app-config are used for signing and -verification. If you are on the new backend system and are not using legacy -plugins using the compatibility wrapper, you can skip this section. - -### Configuration (legacy) - -In local development, there is no need to configure anything for this auth -method. But in production, you must configure at least one legacy type external -access method: - -```yaml title="in e.g. app-config.production.yaml" -backend: - auth: - externalAccess: - - type: legacy - options: - secret: my-secret-key-catalog - subject: legacy-catalog - - type: legacy - options: - secret: my-secret-key-scaffolder - subject: legacy-scaffolder -``` - -The old style keys config is also supported as an alternative, but please -consider using the new style above instead: - -```yaml title="in e.g. app-config.production.yaml" -backend: - auth: - keys: - - secret: my-secret-key-catalog - - secret: my-secret-key-scaffolder -``` - -The secrets must be any base64-encoded random data, but for security reasons -should be sufficiently long so as not to be easy to guess by brute force. You -can for example generate them on the command line: - -```shell -node -p 'require("crypto").randomBytes(24).toString("base64")' -``` - -The subjects must be strings without whitespace. They are used for identifying -each caller, and become part of the credentials object that request recipient -plugins get. - -In both of the examples we showed two secrets being specified, but the minimum -is one. The order is significant: the first one is always used for signing of -outgoing requests to other backend plugins, while all of the keys are used for -verification. This is useful if you want to be able to have unique keys per -deployment if you are using split deployments of Backstage. Then each deployment -lists its own signing secret at the top, and only adds the secrets for those -other deployments that it wants to permit to call it. - -For most organizations, we recommend leaving it at just one key and -[migrating](../backend-system/building-backends/08-migrating.md) to the new -backend system as soon as possible instead of experimenting with multiple legacy -secrets. - -### External Callers (legacy) - -For legacy Backstage backend plugins, the above configuration is enough. But -external callers who wish to make requests using this flow must generate tokens -according to the following rules. - -The token must be a JWT with a `HS256` signature, using the raw base64 decoded -value of the configured key as the secret. It must also have the following -payload: - -- `sub`: the exact string "backstage-server" -- `exp`: one hour from the time it was generated, in epoch seconds - -:::note Note - -The JWT must encode the `alg` header as a protected header, such as with -[setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader). - -::: - -The caller then passes along the JWT token with requests in the `Authorization` -header: - -```yaml -Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW -``` - ## Access Restrictions Each `externalAccess` entry may optionally have an `accessRestrictions` key, diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 293a46b7ca..6e1012dce0 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -2,12 +2,12 @@ id: index title: Backend System Architecture sidebar_label: Overview -description: The structure and architecture of the new Backend System and its component parts +description: The structure and architecture of the Backend System and its component parts --- ## Building Blocks -This section introduces the high-level building blocks upon which this new +This section introduces the high-level building blocks upon which this system is built. These are all concepts that exist in our current system in one way or another, but they have all been lifted up to be first class concerns in the new system. Regardless of whether you are setting up your own backstage diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index a38c1d44e5..3fb9ea60a9 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -2,7 +2,7 @@ id: index title: Building Backends sidebar_label: Overview -description: Building backends using the new backend system +description: Building backends using the backend system --- :::note Note diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 3aa06216b4..fdbac8e65f 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -1340,105 +1340,3 @@ const backend = createBackend(); backend.add(import('@backstage/plugin-kubernetes-backend')); /* highlight-add-end */ ``` - -### The Plugins in Backstage Repo - -The vast majority of the backend plugins that currently live in the Backstage Repo have been migrated and their respective `README`s have details on how they should be installed using the New Backend System. - -| Package | Role | Migrated | Uses Alpha Export | Link to `README` | -| ------------------------------------------------------------------ | --------------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| @backstage-community/plugin-adr-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/adr/plugins/adr-backend/README.md) | -| @backstage-community/plugin-airbrake-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/airbrake/plugins/airbrake-backend/README.md) | -| @backstage/plugin-app-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/app-backend/README.md) | -| @backstage/plugin-auth-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) | -| @backstage/plugin-auth-backend-module-atlassian-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-atlassian-provider/README.md) | -| @backstage/plugin-auth-backend-module-aws-alb-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-aws-alb-provider/README.md) | -| @backstage/plugin-auth-backend-module-gcp-iap-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gcp-iap-provider/README.md) | -| @backstage/plugin-auth-backend-module-github-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-github-provider/README.md) | -| @backstage/plugin-auth-backend-module-gitlab-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gitlab-provider/README.md) | -| @backstage/plugin-auth-backend-module-google-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-google-provider/README.md) | -| @backstage/plugin-auth-backend-module-guest-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-guest-provider/README.md) | -| @backstage/plugin-auth-backend-module-microsoft-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-microsoft-provider/README.md) | -| @backstage/plugin-auth-backend-module-oauth2-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-provider/README.md) | -| @backstage/plugin-auth-backend-module-oauth2-proxy-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-proxy-provider/README.md) | -| @backstage/plugin-auth-backend-module-oidc-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oidc-provider/README.md) | -| @backstage/plugin-auth-backend-module-okta-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-okta-provider/README.md) | -| @backstage/plugin-auth-backend-module-pinniped-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-pinniped-provider/README.md) | -| @backstage/plugin-auth-backend-module-vmware-cloud-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-vmware-cloud-provider/README.md) | -| @backstage-community/plugin-azure-devops-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-devops/plugins/azure-devops-backend/README.md) | -| @backstage-community/plugin-azure-sites-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-sites/plugins/azure-sites-backend/README.md) | -| @backstage-community/plugin-badges-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/badges/plugins/badges-backend/README.md) | -| @backstage-community/plugin-bazaar-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/bazaar/plugins/bazaar-backend/README.md) | -| @backstage/plugin-catalog-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/README.md) | -| @backstage/plugin-catalog-backend-module-aws | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-aws/README.md) | -| @backstage/plugin-catalog-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-azure/README.md) | -| @backstage/plugin-catalog-backend-module-backstage-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-backstage-openapi/README.md) | -| @backstage/plugin-catalog-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-catalog-backend-module-bitbucket-server | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-server/README.md) | -| @backstage/plugin-catalog-backend-module-gcp | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gcp/README.md) | -| @backstage/plugin-catalog-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gerrit/README.md) | -| @backstage/plugin-catalog-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github/README.md) | -| @backstage/plugin-catalog-backend-module-github-org | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github-org/README.md) | -| @backstage/plugin-catalog-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gitlab/README.md) | -| @backstage/plugin-catalog-backend-module-incremental-ingestion | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-incremental-ingestion/README.md) | -| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) | -| @backstage/plugin-catalog-backend-module-msgraph | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md) | -| @backstage/plugin-catalog-backend-module-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-openapi/README.md) | -| @backstage/plugin-catalog-backend-module-puppetdb | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) | -| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-scaffolder-entity-model/README.md) | -| @backstage/plugin-catalog-backend-module-unprocessed | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-unprocessed/README.md) | -| @backstage-community/plugin-code-coverage-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/code-coverage/plugins/code-coverage-backend/README.md) | -| @backstage/plugin-devtools-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/devtools-backend/README.md) | -| @backstage-community/plugin-entity-feedback-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/entity-feedback/plugins/entity-feedback-backend/README.md) | -| @backstage/plugin-events-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md) | -| @backstage/plugin-events-backend-module-aws-sqs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-aws-sqs/README.md) | -| @backstage/plugin-events-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-azure/README.md) | -| @backstage/plugin-events-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-events-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gerrit/README.md) | -| @backstage/plugin-events-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-github/README.md) | -| @backstage/plugin-events-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) | -| @internal/plugin-todo-list-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend/README.md) | -| @backstage-community/plugin-explore-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/explore/plugins/explore-backend/README.md) | -| @backstage-community/plugin-jenkins-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/jenkins/plugins/jenkins-backend/README.md) | -| @backstage-community/plugin-kafka-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/kafka/plugins/kafka-backend/README.md) | -| @backstage/plugin-kubernetes-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/kubernetes-backend/README.md) | -| @backstage-community/plugin-lighthouse-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/lighthouse/plugins/lighthouse-backend/README.md) | -| @backstage-community/plugin-linguist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/linguist/plugins/linguist-backend/README.md) | -| @backstage-community/plugin-nomad-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/nomad/plugins/nomad-backend/README.md) | -| @backstage/plugin-notifications-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/notifications-backend/README.md) | -| @backstage-community/plugin-periskop-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/periskop/plugins/periskop-backend/README.md) | -| @backstage/plugin-permission-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend/README.md) | -| @backstage/plugin-permission-backend-module-allow-all-policy | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend-module-policy-allow-all/README.md) | -| @backstage-community/plugin-playlist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/playlist/plugins/playlist-backend/README.md) | -| @backstage/plugin-proxy-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/proxy-backend/README.md) | -| @backstage-community/plugin-rollbar-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/rollbar/plugins/rollbar-backend/README.md) | -| @backstage/plugin-scaffolder-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/README.md) | -| @backstage/plugin-scaffolder-backend-module-azure | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-azure/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket-server | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-server/README.md) | -| @backstage/plugin-scaffolder-backend-module-confluence-to-markdown | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-confluence-to-markdown/README.md) | -| @backstage/plugin-scaffolder-backend-module-cookiecutter | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-cookiecutter/README.md) | -| @backstage/plugin-scaffolder-backend-module-gerrit | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gerrit/README.md) | -| @backstage/plugin-scaffolder-backend-module-gitea | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitea/README.md) | -| @backstage/plugin-scaffolder-backend-module-github | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/README.md) | -| @backstage/plugin-scaffolder-backend-module-gitlab | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitlab/README.md) | -| @backstage/plugin-scaffolder-backend-module-rails | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/README.md) | -| @backstage/plugin-scaffolder-backend-module-sentry | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-sentry/README.md) | -| @backstage/plugin-scaffolder-backend-module-yeoman | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-yeoman/README.md) | -| @backstage/plugin-search-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend/README.md) | -| @backstage/plugin-search-backend-module-catalog | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-catalog/README.md) | -| @backstage/plugin-search-backend-module-elasticsearch | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-elasticsearch/README.md) | -| @backstage/plugin-search-backend-module-explore | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-explore/README.md) | -| @backstage/plugin-search-backend-module-pg | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-pg/README.md) | -| @backstage/plugin-search-backend-module-stack-overflow-collator | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/README.md) | -| @backstage/plugin-search-backend-module-techdocs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-techdocs/README.md) | -| @backstage/plugin-signals-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/signals-backend/README.md) | -| @backstage-community/plugin-sonarqube-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/sonarqube/plugins/sonarqube-backend/README.md) | -| @backstage-community/plugin-stack-overflow-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/stack-overflow/plugins/stack-overflow-backend/README.md) | -| @backstage-community/plugin-tech-insights-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/tech-insights/plugins/tech-insights-backend/README.md) | -| @backstage-community/plugin-tech-insights-backend-module-jsonfc | backend-plugin-module | true | | [README](https://github.com/backstage/community-plugins/blob/main/workspaces/tech-insights/plugins/tech-insights-backend-module-jsonfc/README.md) | -| @backstage/plugin-techdocs-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/README.md) | -| @backstage-community/plugin-todo-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/todo/plugins/todo-backend/README.md) | -| @backstage/plugin-user-settings-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/user-settings-backend/README.md) | -| @backstage-community/plugin-vault-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/vault/plugins/vault-backend/README.md) | diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 53bf4af8ef..3b0696aa8e 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -2,16 +2,9 @@ id: index title: Building Backend Plugins and Modules sidebar_label: Overview -description: Building backend plugins and modules using the new backend system +description: Building backend plugins and modules using the backend system --- -:::note Note - -If you have an existing backend and/or backend plugins that are not yet -using the new backend system, see [migrating](./08-migrating.md). - -::: - This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and [modules](../architecture/06-modules.md). They are sometimes collectively referred to as backend _features_, and are the building blocks that adopters add to their diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 50ebea0e4f..7da7fc1efa 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -24,6 +24,7 @@ import { coreServices } from '@backstage/backend-plugin-api'; - [Identity Service](./identity.md) - Deprecated user authentication service, use the [Auth Service](./auth.md) instead. - [Lifecycle Service](./lifecycle.md) - Registration of plugin startup and shutdown lifecycle hooks. - [Logger Service](./logger.md) - Plugin-level logging. +- [Metrics Service](./metrics.md) - Plugin-scoped metrics instrumentation (alpha). - [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions. - [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin. - [Root Config Service](./root-config.md) - Access to static configuration. diff --git a/docs/backend-system/core-services/actions-registry.md b/docs/backend-system/core-services/actions-registry.md index 9e493e6bb2..e8e2dc7bb2 100644 --- a/docs/backend-system/core-services/actions-registry.md +++ b/docs/backend-system/core-services/actions-registry.md @@ -25,6 +25,7 @@ Each action registered with the service must conform to the `ActionsRegistryActi ### Optional Properties +- **`visibilityPermission`:** A `BasicPermission` that controls visibility and access to the action through the permissions framework. See [Permissions](#permissions) below. - **`attributes`:** Object containing behavioral flags: - **`destructive`:** Boolean indicating if the action modifies or deletes data - **`idempotent`:** Boolean indicating if running the action multiple times produces the same result @@ -157,6 +158,43 @@ export const myPlugin = createBackendPlugin({ }); ``` +## Permissions + +Actions can optionally declare a `visibilityPermission` to control visibility and access through the Backstage permissions framework. The `visibilityPermission` must be a `BasicPermission` (not a resource permission). When set, the action is only visible in listings and accessible by callers who are authorized. + +When accessed via the Actions Service or the `/.backstage/actions/v1/...` HTTP endpoints, actions that are denied by the permission policy are filtered from list results and return a `404 Not Found` on invocation, as if they don't exist. + +Permissions declared on actions are automatically registered with the `PermissionsRegistryService` so they appear in the permission policy system. + +### Adding a Permission to an Action + +```typescript +import { createPermission } from '@backstage/plugin-permission-common'; + +// Define a permission for your action +const myDeletePermission = createPermission({ + name: 'my-plugin.actions.deleteEntity', + attributes: { action: 'delete' }, +}); + +actionsRegistry.register({ + name: 'delete-entity', + title: 'Delete Entity', + description: 'Removes an entity from the catalog', + visibilityPermission: myDeletePermission, + schema: { + input: z => z.object({ entityRef: z.string() }), + output: z => z.object({ deleted: z.boolean() }), + }, + action: async ({ input }) => { + // action logic + return { output: { deleted: true } }; + }, +}); +``` + +Actions without a `visibilityPermission` field remain visible and accessible by all callers, preserving backwards compatibility. + ## Best Practices ### Naming Conventions diff --git a/docs/backend-system/core-services/actions.md b/docs/backend-system/core-services/actions.md index 02f8b65591..fc2eabc0f3 100644 --- a/docs/backend-system/core-services/actions.md +++ b/docs/backend-system/core-services/actions.md @@ -65,6 +65,10 @@ backend: - 'scaffolder.internal.*' ``` +### Permissions + +Actions registered with a `visibilityPermission` field are automatically checked against the permissions framework. When listing actions, any actions denied by the active permission policy are filtered out of the results. When invoking a denied action, a `404 Not Found` error is returned. See the [Actions Registry Permissions](./actions-registry.md#permissions) documentation for how to configure permissions on actions. + ## Using the Service ### Listing Available Actions diff --git a/docs/backend-system/core-services/auditor.md b/docs/backend-system/core-services/auditor.md index fe4ecd196c..2d0864eeca 100644 --- a/docs/backend-system/core-services/auditor.md +++ b/docs/backend-system/core-services/auditor.md @@ -146,8 +146,8 @@ To clarify how to utilize the Auditor feature effectively, we recommend explorin - **Code Implementation Example (createRouter.ts):** - The [`createRouter.ts`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/createRouter.ts) file within the Catalog Backend showcases a practical integration of the `AuditorService` within a Backstage backend plugin. - Specifically, the lines that demonstrate the creation of an audit event. This includes setting critical parameters such as `eventId` and `severityLevel`, as well as incorporating relevant metadata like `queryType` and `entityRef`. -- **Documentation Example (README.md):** - - The "Audit Events" section of the Catalog Backend's [`README.md`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/README.md#audit-events) provides a well-structured example of documenting emitted audit events. +- **Documentation Example:** + - The "Audit Events" section of the Software Catalog [Audit Documentation](../../features/software-catalog/audit-events.md) provides a well-structured example of documenting emitted audit events. - It illustrates how to detail various `eventId` values and their corresponding `meta` fields (e.g., `queryType`, `actionType`) for different plugin operations. These examples provide both a code-level demonstration and a documentation guideline for effectively utilizing the `AuditorService` to manage audit events within your Backstage plugins. diff --git a/docs/backend-system/core-services/metrics.md b/docs/backend-system/core-services/metrics.md new file mode 100644 index 0000000000..6396143fad --- /dev/null +++ b/docs/backend-system/core-services/metrics.md @@ -0,0 +1,175 @@ +--- +id: metrics +title: Metrics Service (alpha) +sidebar_label: Metrics Service (alpha) +description: Documentation for the Metrics service +--- + +The Metrics Service provides a unified interface for emitting application-level metrics from Backstage backend plugins. It wraps the [OpenTelemetry](https://opentelemetry.io/) Meter API and automatically scopes each plugin's metrics using the OpenTelemetry [Instrumentation Scope](https://opentelemetry.io/docs/concepts/instrumentation-scope/), so telemetry backends can identify which plugin produced each metric. + +:::note +This service is currently in **alpha** and is imported from `@backstage/backend-plugin-api/alpha`. The API may change in future releases. +::: + +## Setting up OpenTelemetry + +The Metrics Service does **not** configure the OpenTelemetry SDK itself. You are responsible for initializing the OpenTelemetry Node SDK — including exporters, resource attributes, and views — before starting the Backstage backend. Follow the [tutorial](../../tutorials/setup-opentelemetry.md) for more information. + +## How it Relates to OpenTelemetry Auto-Instrumentation + +The Metrics Service **complements** auto-instrumentation rather than replacing it. Auto-instrumentation captures infrastructure-level signals like HTTP request counts and durations automatically. The Metrics Service is for **application-level metrics** that only your plugin can provide — things like entities processed, tasks completed, or active sessions. + +```ts +// Auto-instrumentation provides automatically: +// http.server.request.duration{method="GET", route="/catalog/entities"} + +// MetricsService provides manually: +const processed = metrics.createCounter('entities.processed.total', { + description: 'Total entities processed during refresh', +}); +processed.add(entities.length, { operation: 'refresh' }); +``` + +## Using the Service + +Since the Metrics Service is an alpha API, the service reference is imported from `@backstage/backend-plugin-api/alpha` instead of `coreServices`. + +```ts +import { createBackendPlugin } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; + +createBackendPlugin({ + pluginId: 'todos', + register(env) { + env.registerInit({ + deps: { + metrics: metricsServiceRef, + }, + async init({ metrics }) { + const todoCount = metrics.createCounter('todos.total', { + description: 'Total number of todos', + }); + + // Later, when adding a todo: + todoCount.add(1, { 'todo.category': 'personal' }); + }, + }); + }, +}); +``` + +## Instrument Types + +The service provides both synchronous and observable (asynchronous) instrument types, matching the OpenTelemetry specification. + +### Synchronous Instruments + +Synchronous instruments are used inline where the measurement occurs. + +| Method | Description | Example Use Case | +| --------------------- | ------------------------------------------------------ | ---------------------------------- | +| `createCounter` | Monotonically increasing sum (non-negative increments) | Total requests, entities processed | +| `createUpDownCounter` | Sum that can increase or decrease | Active connections, queue depth | +| `createHistogram` | Distribution of values (e.g. durations, sizes) | Request latency, payload sizes | +| `createGauge` | Point-in-time value | CPU usage, memory utilization | + +```ts +const counter = metrics.createCounter('todos.completed.total', { + description: 'Total todos completed', +}); +counter.add(1, { 'todo.status': 'completed' }); + +const histogram = metrics.createHistogram('todo.duration', { + description: 'Time spent processing a todo', + unit: 'seconds', + advice: { explicitBucketBoundaries: [0.01, 0.05, 0.1, 0.5, 1, 5] }, +}); +histogram.record(durationInSeconds, { + 'todo.category': 'personal', + 'todo.status': 'completed', +}); + +const upDown = metrics.createUpDownCounter('todos.in_flight', { + description: 'Number of todos currently in flight', +}); +upDown.add(1); +// ... later +upDown.add(-1); +``` + +### Observable Instruments + +Observable instruments use callbacks that are invoked when a metric collection occurs. This is useful for metrics that are expensive to compute or that come from external sources like databases. + +| Method | Description | Example Use Case | +| ------------------------------- | --------------------------------------------------- | -------------------------------------- | +| `createObservableCounter` | Monotonically increasing sum, reported via callback | Total items ingested from external API | +| `createObservableUpDownCounter` | Sum that can go up or down, reported via callback | Connection pool size | +| `createObservableGauge` | Point-in-time value, reported via callback | Row counts, cache hit ratios | + +```ts +const entityCount = metrics.createObservableGauge('catalog.entities.count', { + description: 'Total amount of entities in the catalog', +}); + +entityCount.addCallback(async gauge => { + const results = await getEntityCountsByKind(); + for (const { kind, count } of results) { + gauge.observe(count, { kind }); + } +}); +``` + +## Metric Options + +All `create*` methods accept an optional `MetricOptions` object: + +| Property | Type | Description | +| ------------- | -------- | -------------------------------------------------------------------- | +| `description` | `string` | Human-readable description of the metric | +| `unit` | `string` | Unit of measurement (e.g. `'seconds'`, `'{entity}'`, `'bytes'`) | +| `advice` | `object` | Aggregation hints, such as `explicitBucketBoundaries` for histograms | + +## Type-Safe Attributes + +Metric instruments accept a generic type parameter that constrains the attributes passed to `add`, `record`, or `observe`: + +```ts +interface TodoAttributes { + 'todo.category': string; + 'todo.status': 'completed' | 'in_progress' | 'blocked'; +} + +const completed = metrics.createCounter( + 'todos.completed.total', + { description: 'Total todos completed' }, +); + +// Type-safe attributes are enforced +completed.add(1, { 'todo.category': 'personal', 'todo.status': 'completed' }); +``` + +## Advanced Configuration + +The service reads optional configuration from `app-config.yaml` under `backend.metrics.plugin..meter`. This lets operators override the OpenTelemetry Instrumentation Scope for a specific plugin without code changes. + +:::tip +Each plugin automatically receives a meter named `backstage-plugin-`. You typically won't need to configure it. +::: + +```yaml +backend: + metrics: + plugin: + catalog: + meter: + name: 'custom-catalog-meter' + version: '2.0.0' + schemaUrl: 'https://example.com/schema' +``` + +| Property | Type | Default | Description | +| ----------- | -------- | ----------------------------- | ------------------------------- | +| `name` | `string` | `backstage-plugin-` | Name of the OpenTelemetry meter | +| `version` | `string` | — | Version string for the meter | +| `schemaUrl` | `string` | — | Schema URL for the meter | diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index cb53ebe81c..107fde3d57 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -156,7 +156,6 @@ These colors form a layered neutral scale for your application backgrounds. `--b | Token Name | Description | | ----------------------------- | ------------------------------------------------------------ | | `--bui-bg-app` | The base background color of your Backstage instance. | -| `--bui-bg-popover` | The background color used for popovers, tooltips, and menus. | | `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. | | `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. | | `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. | diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index f58afedee8..f1be29c312 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -55,12 +55,10 @@ defined in [`package.json`](https://github.com/backstage/backstage/blob/master/package.json): ```json - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], ``` Let's look at them individually. @@ -146,7 +144,10 @@ are separated out into their own folder, see further down. - [`dev-utils/`](https://github.com/backstage/backstage/tree/master/packages/dev-utils) - Helps you setup a plugin for isolated development so that it can be served - separately. + separately. This is for plugins using the legacy frontend system. + +- [`frontend-dev-utils/`](https://github.com/backstage/backstage/tree/master/packages/frontend-dev-utils) - + Utilities for developing frontend plugins using the new frontend system. Provides the `createDevApp` helper for setting up a minimal development app in a plugin's `dev/` entry point. - [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) - Another CLI that can be run to try out what would happen if you build all the diff --git a/docs/features/kubernetes/authenticationstrategy.md b/docs/features/kubernetes/authenticationstrategy.md index e39201aaeb..5c20965bde 100644 --- a/docs/features/kubernetes/authenticationstrategy.md +++ b/docs/features/kubernetes/authenticationstrategy.md @@ -157,7 +157,7 @@ export class AwsIamStrategy implements AuthenticationStrategy { Sometimes you need to add a new way to authenticate against a kubernetes cluster not support by default by Backstage. This is how integrators can bring their own kubernetes auth strategies through the use of the [`addAuthStrategy`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts#L211) method on `KubernetesBuilder` or through the [AuthStrategyExtensionPoint](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/kubernetes-backend/src/plugin.ts#L112). So, on the following sections, we are going to introduce a new AuthStrategy for [Pinniped][1], an authentication service for Kubernetes clusters. -### Custom Pinniped auth strategy in the new backend system +### Custom Pinniped auth strategy in the backend system To add a new AuthStrategy, we need to create a new Pinniped [backend module](../../backend-system/building-plugins-and-modules/01-index.md#modules) to extend the Kubernetes-Backend plugin. The Pinniped module will interact with the Kubernetes-Backend plugin through the [extension points](../../backend-system/architecture/05-extension-points.md) registered by the plugin. The Kubernetes-Backend plugin [registers](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-backend/src/plugin.ts#L155) multiple extension points like `kubernetesObjectsProvider`, `kubernetesClusterSupplier`, `kubernetesFetcher`, `kubernetesServiceLocator` and the `kubernetesAuthStrategy`. diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index ca8b8f638d..2a0e20cbdc 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -150,7 +150,54 @@ Some more real world usable examples: #### Full text filtering -TODO +You can perform a text search across entity fields using the `fullTextFilterTerm` +query parameter. This performs a case-insensitive substring match against the +values in the entity YAML fields. + +By default, when no `fullTextFilterFields` parameter is specified, the search +runs against the current sort field (from `orderField`), or `metadata.uid` if +no sort field is set. This means that without specifying fields explicitly, the +search may not match against the fields you expect. + +To control which fields are searched, pass the `fullTextFilterFields` query +parameter as a comma-separated list of entity field paths. + +Query parameters: + +- `fullTextFilterTerm` - The text to search for (case insensitive, substring match) +- `fullTextFilterFields` - A comma-separated list of entity field paths to + search against (e.g. `metadata.name,metadata.title`) + +Example: + +```text +/entities/by-query?fullTextFilterTerm=my-service&fullTextFilterFields=metadata.name,metadata.title + + Return entities whose metadata.name OR metadata.title contains "my-service" +``` + +Some more real world usable examples: + +- Search for components by name: + + `/entities/by-query?filter=kind=component&fullTextFilterTerm=payment&fullTextFilterFields=metadata.name` + +- Search across both name and title: + + `/entities/by-query?filter=kind=system&fullTextFilterTerm=platform&fullTextFilterFields=metadata.name,metadata.title` + +- Combine with other filters (e.g. owned by a specific group): + + `/entities/by-query?filter=kind=component,relations.ownedBy=group:default/my-team&fullTextFilterTerm=api&fullTextFilterFields=metadata.name` + +:::note Note + +Full text filtering is mutually exclusive with cursor-based pagination. When a +`cursor` is provided, `fullTextFilterTerm` and `fullTextFilterFields` are +ignored — the cursor already encodes the original filter parameters from the +initial request. + +::: #### Field selection @@ -210,6 +257,191 @@ if `prevCursor` exists, it can be used to retrieve the previous batch of entitie it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. +### `POST /entities/by-query` + +This supports the same features as the `GET` variant, but in a `POST` body to +not have to abide by URL length limits. Additionally, it supports advanced, more +expressive querying format - see below. The response format is identical. + +#### Querying by filter predicate + +You can pass in a filter predicate to select a subset of entities in the +catalog. They are comprised of an optional logical expression tree (using +`$all`, `$any`, `$not`), ending in filter sets that can have custom matchers +(e.g. `$exists`, `$in`, `$hasPrefix`, `$contains`). + +This is an example of what such a filter predicate expression might look like: + +```js +{ + "query": { + "$all": [ + { + "kind": "Component", + "spec.type": { "$in": ["service", "website"] } + }, + { + "$not": { + "metadata.annotations.backstage.io/orphan": "true" + } + } + ] + } +} +``` + +A filter set is an object whose keys are dot separated paths into an object, and +the values are either primitives (string, number, or boolean) or custom matchers +as per below. An example of a simple such filter set is: + +```js +// All of the following must be true for a given entity (there's an +// implicit AND between them) +{ + // The kind field is matched against a literal, case insensitively + "kind": "Component", + // The type field inside the spec is matched using a custom matcher, see below + "spec.type": { "$in": ["service", "website"] } +} +``` + +The root of the query is always an object, whether there is a logic expression +tree or not. Nodes with a single key that starts with a `$` sign have special +meaning. + +- `$not`: Logical negation. + + Its value must be a single expression. Example: + + ```js + // Matches entities that do NOT have kind Component + { + "$not": { + "kind": "Component", + } + } + ``` + + Note that `$not` cannot be used in a right hand side value matcher. + + ```js + // ❌ WRONG + { "kind": { "$not": "Component" } } + // ✅ CORRECT + { "$not": { "kind": "Component" } } + ``` + +- `$all`: Require that all given expressions match each entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that BOTH have kind Component and type website + { + "$all": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array always matches every entity. + +- `$any`: Require that at least one of a set of expressions match a given entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that EITHER have kind Component or type website + { + "$any": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array never matches anything. + +- `$exists`: Assert on the existence of fields. + + Its value is either `true`, meaning that the field must exist on the entity + (no matter what its value), or `false`, meaning that it must not exist. + Example: + + ```js + // Matches entities that DO NOT have that annotation, ignoring what the + // value might be + { + "metadata.annotations.backstage.io/orphan": { + "$exists": false + }, + } + ``` + +- `$in`: Assert that a field has any of a set of primitive values. + + Its value must be an array of string, number, and/or boolean values. Example: + + ```js + // Matches entities whose type is EITHER service or website + { + "spec.type": { + "$in": ["service", "website"] + } + } + ``` + + The matching is case insensitive. An empty array never matches anything. + +- `$hasPrefix`: Assert that a field is a string that starts with a certain prefix text. + + Its value is a string. Example: + + ```js + // Matches entities whose project slug annotation starts with "backstage/" + { + "metadata.annotations.github.com/project-slug": { + "$hasPrefix": "backstage/" + } + } + ``` + + The matching is case insensitive, and captures both exact matches and strings + that start with the given prefix. + +- `$contains`: Assert that an array contains an element that matches the given expression. + + There is only limited support for this matcher. One use case is for relations: + + ```js + { + // Specifically type and (optionally) targetRef supported, and only + // with equality or "$in" for the targetRef + "relations": { + "$contains": { + "type": "ownedBy", + "targetRef": { + "$in": ["user:default/foo", "group:default/bar"] + } + } + } + } + ``` + + The other use case is for arrays where you match with a primitive value, such + as tags. Example: + + ```js + { + // Works for array fields whose items are primitive values + // (typically strings, but numbers and booleans are also supported) + "metadata.tags": { + "$contains": "java" + } + } + ``` + ### `GET /entities` Lists entities. diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index d62bb34505..2e984d2942 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -728,6 +728,49 @@ Notes: - Icons for groups and tabs are resolved via the app's IconsApi. When using a string icon id (for example `"dashboard"`), ensure that the corresponding icon bundles are enabled/installed in your app (see the [IconBundleBlueprint documentation](https://backstage.io/api/stable/variables/_backstage_plugin-app-react.IconBundleBlueprint.html)). - Group icons are only rendered if `showNavItemIcons` is set to `true`. +### Content ordering within groups + +By default, content items within each group are sorted alphabetically by title. You can change this with the `defaultContentOrder` option, which supports two modes: + +- **`title`** (default) — sort alphabetically by the content extension's title (case-insensitive). +- **`natural`** — preserve the natural extension discovery/registration order. + +A page-level `defaultContentOrder` sets the default for all groups, and individual groups can override it with a per-group `contentOrder`: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + # Default content order for all groups + defaultContentOrder: title + + groups: + - documentation: + title: Docs + # Override: keep natural order for this group + contentOrder: natural +``` + +Note that content ordering only applies to content items within groups. Ungrouped tabs (those not matching any group definition) always retain their natural order. + +### Group aliases + +Groups can declare `aliases` — a list of other group IDs that should be treated as equivalent. Any entity content extension targeting an aliased group ID will be included in the aliasing group. This is useful when renaming or merging groups without having to reconfigure individual extensions: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + groups: + - develop: + title: Develop + # Content targeting 'development' will appear in this group + aliases: + - development +``` + ### Overriding or disabling a tab's group (per extension) Each entity content extension (tabs on the entity page) can declare a default `group` in code. You can override or disable this per installation in `app-config.yaml` using the extension's config: diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 3ed027a0a4..6ceb00b3b5 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -366,3 +366,82 @@ Now install your module. ```ts title="packages/backend/src/index.ts" backend.add(eventsModuleCatalogErrors); ``` + +## OpenAPI and AsyncAPI Placeholder Support + +The **OpenAPI Catalog Backend Module** registers a JSON Schema placeholder resolver for the `openapi` (and `asyncapi`) placeholder keys. This enables you to use `$openapi` and `$asyncapi` references in your catalog entities, while having all underlying `$ref` pointers resolved and bundled as part of the schema processing. + +### Installation + +1. Add the package to your backend: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-openapi +``` + +2. Register the module in your backend: + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend-module-openapi')); +``` + +### Usage + +To trigger the `$ref` resolution, use the `$openapi` (or `$asyncapi`) placeholder in your catalog entity definition: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: example + description: Example API +spec: + type: openapi + lifecycle: production + owner: team + definition: + $openapi: ./spec/openapi.yaml # by using $openapi Backstage will now resolve all $ref instances +``` + +## Backstage OpenAPI Module + +As Backstage increasingly uses OpenAPI to define its core APIs (such as the Catalog and Scaffolder), discovering and interacting with these APIs is essential for integrating external tools. + +You can install the **Backstage OpenAPI Module** to easily expose the OpenAPI specifications for your Backstage instance plugins directly into the catalog. + +### Installation + +1. Install the module in your backend: + +```bash +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-backstage-openapi +``` + +2. Register the module in your backend: + +```ts title="packages/backend/src/index.ts" +backend.add( + import('@backstage/plugin-catalog-backend-module-backstage-openapi'), +); +``` + +3. Add the configuration to your `app-config.yaml`: + +```yaml title="app-config.yaml" +catalog: + providers: + backstageOpenapi: + plugins: + - catalog + - scaffolder + # Optional configuration: + # definitionFormat controls how generated definitions are serialized. + # Supported values: 'json' (default) or 'yaml'. + # definitionFormat: json + # entityOverrides can be used to override parts of the produced entities. + # For example, to add a tag to all generated APIs: + # entityOverrides: + # metadata: + # tags: + # - from-openapi +``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index ac8baadad5..f9ab41e37a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -607,7 +607,7 @@ component, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.system` [optional] @@ -811,7 +811,7 @@ Template, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ## Kind: API @@ -921,7 +921,7 @@ one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.system` [optional] @@ -1121,7 +1121,7 @@ Describes the following entity kind: | `apiVersion` | `backstage.io/v1alpha1` | | `kind` | `Resource` | -A resource describes the infrastructure a system needs to operate, like BigTable +A Resource describes the infrastructure a system needs to operate, like BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems allows to visualize resource footprint, and create tooling around them. @@ -1164,7 +1164,7 @@ resource, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.type` [required] @@ -1262,7 +1262,7 @@ but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.domain` [optional] @@ -1334,7 +1334,7 @@ but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.subdomainOf` [optional] diff --git a/docs/features/software-templates/authorizing-scaffolder-template-details.md b/docs/features/software-templates/authorizing-scaffolder-template-details.md index 7ff2cd9377..e35d81d260 100644 --- a/docs/features/software-templates/authorizing-scaffolder-template-details.md +++ b/docs/features/software-templates/authorizing-scaffolder-template-details.md @@ -6,7 +6,13 @@ description: How to authorize parts of a template and authorize scaffolder task The scaffolder plugin integrates with the Backstage [permission framework](../../permissions/overview.md), which allows you to control access to certain parameters and steps in your templates based on the user executing the template. It also allows you to control access to scaffolder tasks. -### Authorizing parameters and steps +:::tip + +To better understand the following examples make sure to review the [Writing a permission policy](../../permissions/writing-a-policy.md)) documentation before moving forward. + +::: + +## Authorizing parameters and steps To mark specific parameters or steps as requiring permission, add the `backstage:permissions` property to the parameter or step with one or more tags. For example: @@ -288,62 +294,3 @@ All other users are granted permissions to perform/access the following actions/ - Read scaffolder tasks and their associated events/logs for tasks created by the user. Although the rules exported by the scaffolder are simple, combining them can help you achieve more complex use cases. - -### Authorizing in the New Backend System - -Instead of the changes in `permission.ts` noted in the above example you will make them in your `index.ts`. You will need to create a module where your permission policy will get added. Here is a very simplified example of how to do that: - -```ts title="packages/backend/src/index.ts" -import { createBackendModule } from '@backstage/backend-plugin-api'; -import { - PolicyDecision, - AuthorizeResult, -} from '@backstage/plugin-permission-common'; -import { - PermissionPolicy, - PolicyQuery, - PolicyQueryUser, -} from '@backstage/plugin-permission-node'; -import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; - -class ExamplePermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - user?: PolicyQueryUser, - ): Promise { - // Various scaffolder permission checks ... - - return { - result: AuthorizeResult.ALLOW, - }; - } -} - -const customPermissionBackendModule = createBackendModule({ - pluginId: 'permission', - moduleId: 'allow-all-policy', - register(reg) { - reg.registerInit({ - deps: { policy: policyExtensionPoint }, - async init({ policy }) { - policy.setPolicy(new ExamplePermissionPolicy()); - }, - }); - }, -}); - -const backend = createBackend(); - -// Other plugins... - -/* highlight-add-start */ -backend.add(import('@backstage/plugin-permission-backend')); -backend.add(customPermissionBackendModule); -/* highlight-add-end */ -``` - -:::note Note - -The `ExamplePermissionPolicy` here could be the one from the [Authorizing parameters and steps](#authorizing-parameters-and-steps) example or from the [Authorizing actions](#authorizing-actions) example. It would work the same way for both of them. - -::: diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 22d8ef6454..378c15e18f 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -32,7 +32,7 @@ Here's how to add an action module, first you need to run this command: yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-github ``` -Then you need to add it to your backend, this is a simplified new backend system for example purposes: +Then you need to add it to your backend, this is a simplified backend system for example purposes: ```ts title="/packages/backend/src/index.ts" import { createBackend } from '@backstage/backend-defaults'; diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 845da8e60b..fd4b20d466 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -119,6 +119,36 @@ entity: defaultNamespace: payment ``` +### `autoSelect` + +Whether to automatically select the highlighted option when the input loses focus. Defaults to `true`. + +When set to `false`, users must explicitly select an option from the dropdown by clicking or pressing Enter. This prevents accidental selections when typing to filter options. + +- Default behavior with `autoSelect` as `true` (auto-selects on blur) + +```yaml +entity: + title: Entity + type: string + description: Entity of the component + ui:field: EntityPicker + ui:options: + autoSelect: true +``` + +- Require explicit selection with `autoSelect` as `false` + +```yaml +entity: + title: Entity + type: string + description: Entity of the component + ui:field: EntityPicker + ui:options: + autoSelect: false +``` + ## MultiEntityPicker The input props that can be specified under `ui:options` for the `MultiEntityPicker` field extension. diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 5d9fcba603..64c1acc67d 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -325,6 +325,57 @@ spec: token: ${{ each.value.token }} ``` +### Defining a Secrets Schema + +You can define a JSON Schema for secrets that will be validated when a task is created. This is useful when secrets are passed programmatically (e.g., via CI/CD pipelines or API calls) rather than through the UI form. The schema ensures that required secrets are provided before task execution begins. + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: publish-to-npm + title: Publish to NPM +spec: + owner: backstage/techdocs-core + type: service + + # Define required secrets with a JSON Schema + secrets: + schema: + required: + - NPM_TOKEN + properties: + NPM_TOKEN: + type: string + description: NPM authentication token for publishing + + parameters: + - title: Package Details + properties: + packageName: + type: string + title: Package Name + + steps: + - id: publish + action: npm:publish + input: + packageName: ${{ parameters.packageName }} + token: ${{ secrets.NPM_TOKEN }} +``` + +When a task is created without the required secrets, the API returns a `400` error with a descriptive message: + +```json +{ + "errors": [ + { + "message": "secrets.NPM_TOKEN is required" + } + ] +} +``` + ### Custom step layouts If you find that the default layout of the form used in a particular step does not meet your needs then you can supply your own [custom step layout](./writing-custom-step-layouts.md). diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--new.md index cbafcbe127..170bd80f70 100644 --- a/docs/features/techdocs/addons--new.md +++ b/docs/features/techdocs/addons--new.md @@ -89,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 | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`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.techDocsReportIssueAddonModule.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`). | +| 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! diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 78b472edf2..72e8aa39e8 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -126,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 | -| -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [``](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`). | +| 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! diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 67d29c4ebb..f6be53cb55 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -150,7 +150,7 @@ Options: --legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using /README.md or README.md in case a default /index.md is not provided. (default: false) --runAsDefaultUser Bypass setting the container user as the same user and group id as host for Linux and MacOS (default: false) - -v --verbose Enable verbose output. (default: false) + -v, --verbose Enable verbose output. (default: false) -h, --help display help for command ``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 1246600683..76c231ff0e 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -95,9 +95,6 @@ That's it! Now, we need the TechDocs Backend plugin for the frontend to work. ## Adding TechDocs Backend plugin -:::note -These instructions are for the new backend system. For setting this up in the old backend system, see [here](https://github.com/backstage/backstage/blob/v1.36.1/docs/features/techdocs/getting-started.md) -::: First we need to install the `@backstage/plugin-techdocs-backend` package. ```bash title="From your Backstage root directory" diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 755d88b163..33faf44841 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -852,9 +852,9 @@ and publish the documentation for them. If the value of the `company.com/techdoc annotation is anything other than `'local'`, the user is responsible for publishing documentation to the appropriate location in the TechDocs external storage. -### Hybrid build strategy using the New Backend System +### Hybrid build strategy using the Backend System -To setup a hybrid build strategy using the New Backend System you'll follow the same steps as above but for Step 4 you will need to do the following: +To setup a hybrid build strategy using the Backend System you'll follow the same steps as above but for Step 4 you will need to do the following: ```ts title="packages/backend/src/index.ts" const backend = createBackend(); diff --git a/docs/frontend-system/architecture/10-app.md b/docs/frontend-system/architecture/10-app.md index 0303dd5e70..22457bcb30 100644 --- a/docs/frontend-system/architecture/10-app.md +++ b/docs/frontend-system/architecture/10-app.md @@ -46,31 +46,7 @@ App feature discovery lets you automatically discover and install features provi Because feature discovery needs to interact with the compilation process, it is only available when using the `@backstage/cli` to build your app. It is hooked into the WebPack compilation process by scanning your app package for compatible dependencies, which are then made part of the app compilation bundle. -To enable frontend feature discovery, add the following configuration to your `app-config.yaml`: - -```yaml -app: - packages: all -``` - -This will cause all dependencies in your app package to be installed automatically. If this is not desired, you can use include or exclude filters to narrow down the set of packages: - -```yaml -app: - packages: - # Only the following packages will be included - include: - - '@backstage/plugin-catalog' - - '@backstage/plugin-scaffolder' ---- -app: - packages: - # All but the following package will be included - exclude: - - '@backstage/plugin-catalog' -``` - -Note that you do not need to manually exclude packages that you also import explicitly in code, since plugin instances are deduplicated by the app. You will never end up with duplicate plugin installations except if they are in fact two different plugin instances with different IDs. +For information on how to configure feature discovery and other installation options, see [Installing Plugins](../building-apps/05-installing-plugins.md). ## Plugin Info Resolution diff --git a/docs/frontend-system/architecture/15-plugins.md b/docs/frontend-system/architecture/15-plugins.md index 0918961c73..c2f5f237c3 100644 --- a/docs/frontend-system/architecture/15-plugins.md +++ b/docs/frontend-system/architecture/15-plugins.md @@ -26,6 +26,8 @@ const myPage = PageBlueprint.make({ export default createFrontendPlugin({ pluginId: 'my-plugin', + title: 'My Plugin', + icon: MyPluginIcon, extensions: [myPage], }); ``` @@ -36,6 +38,30 @@ Each plugin needs an ID, which is used to uniquely identify the plugin within an The plugin ID should generally be part of the of the package name and use kebab-case. See both the [frontend naming patterns section](./50-naming-patterns.md), as well as the [package metadata section](../../tooling/package-metadata.md#name) for more information. +### `title` option + +The display title of the plugin, used in page headers and navigation. Falls back to the plugin ID if not provided. + +```tsx +export default createFrontendPlugin({ + pluginId: 'my-plugin', + title: 'My Plugin', + extensions: [...], +}); +``` + +### `icon` option + +The display icon of the plugin, used in page headers and navigation. The type is `IconElement` (`JSX.Element | null`) from `@backstage/frontend-plugin-api`. Icons should be exactly 24x24 pixels in size. + +```tsx +export default createFrontendPlugin({ + pluginId: 'my-plugin', + icon: , + extensions: [...], +}); +``` + ### `extensions` option These are the [extensions](./20-extensions.md) that the plugin provides to the app. Note that you should not export any of these extensions separately from the plugin package, as they can already by accessed via the `getExtension` method of the plugin instance using the extension ID. diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index 8610dfb7b5..bd9a0fbae5 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -63,13 +63,9 @@ Visit the [built-in extensions](#customize-or-override-built-in-extensions) sect Linking routes from different plugins requires this configuration. You can do this either through a configuration file or by coding, visit [this](https://backstage.io/docs/frontend-system/architecture/routes#binding-external-route-references) page for instructions. -### Enable feature discovery +### Install plugins -Use this setting to enable experimental feature discovery when building your app with `@backstage/cli`. With this configuration your application tries to discover and install package extensions automatically, check [here](../architecture/10-app.md#feature-discovery) for more details. - -:::warning -Remember that package extensions that are not auto-discovered must be manually added to the application when creating an app. See [features](#install-features-manually) for more details. -::: +Plugins are typically installed by adding them as dependencies of your app package and relying on feature discovery to automatically detect them. For details on how this works, including how to manually install plugins or control which packages are discovered, see [Installing Plugins](./05-installing-plugins.md). ### Configure extensions individually @@ -83,7 +79,7 @@ Previously you would customize the application routes, components, apis, sidebar ### Install features manually -A manual installation is required if your packages are not discovered automatically, either because you are not using `@backstage/cli` to build your application or because the features are defined in local modules in the app package. In order to manually install a feature, you must import it and pass it to the `createApp` function: +Most plugins are installed automatically through [feature discovery](./05-installing-plugins.md#feature-discovery). Manual installation is needed if your packages are not discovered automatically, either because you are not using `@backstage/cli` to build your application or because the features are defined in local modules in the app package. In order to manually install a feature, you must import it and pass it to the `createApp` function: ```tsx title="packages/app/src/App.tsx" import { createApp } from '@backstage/frontend-defaults'; diff --git a/docs/frontend-system/building-apps/05-installing-plugins.md b/docs/frontend-system/building-apps/05-installing-plugins.md new file mode 100644 index 0000000000..9007ce0eb1 --- /dev/null +++ b/docs/frontend-system/building-apps/05-installing-plugins.md @@ -0,0 +1,71 @@ +--- +id: installing-plugins +title: Installing Plugins +sidebar_label: Installing Plugins +description: How to install frontend plugins in a Backstage app +--- + +Frontend plugins are installed in your Backstage app by adding them as dependencies of your app package. Most of the time this is all you need to do, as the app will automatically discover and install the plugin. + +## Install a plugin package + +To install a plugin, add it as a dependency to your app package. For example, to install the catalog plugin: + +```bash title="From your Backstage root directory" +yarn --cwd packages/app add @backstage/plugin-catalog +``` + +If your app is set up with [feature discovery](#feature-discovery), the plugin will be automatically detected and installed in the app. No additional code changes are needed. + +## Feature discovery + +Feature discovery lets the app automatically discover and install plugins from the dependencies of your app package. This is enabled by setting `app.packages` to `all` in your `app-config.yaml`: + +```yaml title="app-config.yaml" +app: + packages: all +``` + +This is the recommended setup and is the default for all new Backstage apps. With this enabled, any plugin that is added as a dependency of your app package will be automatically discovered and installed. You can use include or exclude filters to control which packages are discovered: + +```yaml title="app-config.yaml" +app: + packages: + include: + - '@backstage/plugin-catalog' + - '@backstage/plugin-scaffolder' +``` + +```yaml title="app-config.yaml" +app: + packages: + exclude: + - '@backstage/plugin-catalog' +``` + +Feature discovery requires that your app is built using the `@backstage/cli`, which is the default for all Backstage apps. Note that you do not need to exclude packages that you also install manually in code, since plugin instances are deduplicated by the app. + +For more details on how feature discovery works under the hood, see the [Feature Discovery](../architecture/10-app.md#feature-discovery) architecture documentation. + +## Manual installation + +If your app does not have [feature discovery](#feature-discovery) enabled, or if you need more control over the plugin installation, you can install plugins manually. This is done by importing the plugin and passing it to `createApp`: + +```tsx title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; + +const app = createApp({ + features: [catalogPlugin], +}); + +export default app.createRoot(); +``` + +Manual installation may also be necessary if you need to control the ordering of plugins, for example when customizing route priorities. Since manually installed plugins are deduplicated against automatically discovered ones, you can safely install a plugin both manually and through feature discovery without causing conflicts. + +If you need to use a 3rd-party plugin that does not yet support the new frontend system, you can use the conversion utilities from `@backstage/core-compat-api` to wrap it. See [Converting 3rd-party Plugins](./06-plugin-conversion.md) for details. + +## Configuring installed plugins + +Once a plugin is installed, you can configure its extensions through the `app.extensions` section of your `app-config.yaml`. See [Configuring Extensions](./02-configuring-extensions.md) for details. diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 3ac5109fc7..2849b7d68b 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -686,7 +686,7 @@ createApp({ #### App Root Sidebar -New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items which are the other `NavItem` extensions provided by the system. +New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items. Nav items are auto-discovered from page extensions registered under `app/routes` (no explicit `NavItemBlueprint` required), with metadata from page config, nav item extensions, or plugin defaults. In order to migrate your existing sidebar, you will want to create an override for the `app/nav` extension. You can do this by copying the standard of having a `src/modules/nav/` folder, which can contain an extension which you can install into the `app` in the form of a `module`. @@ -702,38 +702,45 @@ export const navModule = createFrontendModule({ Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where you implement the entire `Sidebar` component. +The component receives a `navItems` prop with `take(id)` and `rest()` methods for placing specific items in custom positions. The recommended approach is to use `navItems.withComponent(...)` to define a component for rendering each nav item, and then use the returned `take(id)` and `rest()` methods to get pre-rendered elements directly. Items taken from the renderer are also taken from the main list. Keys are automatically assigned when rendering via `rest()`. + ```tsx title="in packages/app/src/modules/nav/Sidebar.tsx" import { NavContentBlueprint } from '@backstage/plugin-app-react'; export const SidebarContent = NavContentBlueprint.make({ params: { - component: ({ items }) => ( - - - } to="/search"> - - - - }> - ... - - - - {/* Items in this group will be scrollable if they run out of space */} - {items.map((item, index) => ( - - ))} - - - - ), + component: ({ navItems }) => { + const nav = navItems.withComponent(item => ( + item.icon} to={item.href} text={item.title} /> + )); + + return ( + + + } to="/search"> + + + + }> + {nav.take('page:catalog')} + {nav.take('page:scaffolder')} + + + {nav.rest({ sortBy: 'title' })} + + + + ); + }, }, }); ``` -The `items` property is a list of all extensions provided by the `NavItemBlueprint` that are currently installed in the App. If you don't want to auto populate this list you can simply remove the rendering of that `SidebarGroup`, but otherwise you can see from the above example how a `SidebarItem` element is rendered for each of the items in the list. +The deprecated `items` prop (a flat list compatible with ``) remains supported for backward compatibility. If you don't want to auto-populate the list, simply remove the rendering of that `SidebarGroup`. -You might also notice that when you're rendering additional fixed icons for plugins that these might become duplicated as the plugin provides a `NavItem` extension and you're also rendering one in the `Sidebar` manually. In order to remove the item from the list of `items` which is passed through, we recommend that you disable that extension using config: +You might also notice that when you're rendering additional fixed icons for plugins (e.g. Search in a dedicated group) these might become duplicated, since that page is also included in `nav.rest()`. To exclude an item from the remaining list, call `nav.take('page:search')` before calling `nav.rest()` — you can discard the return value. Items that have been taken will not appear in `rest()`. + +You can also use the old `NavItemBlueprint`-based nav item extensions to disable items from the nav bar, these can be disabled in config without affecting the page itself: ```yaml title="in app-config.yaml" app: @@ -742,15 +749,6 @@ app: - nav-item:catalog: false ``` -You can also determine the order of the provided auto installed `NavItems` that you get from the system in config. The below example ensures that the `catalog` navigation item will proceed the `search` navigation item when being passed through as the `item` prop. - -```yaml title="in app-config.yaml" -app: - extensions: - - nav-item:catalog - - nav-item:search -``` - #### App Root Routes Your top-level routes are the routes directly under the `AppRouter` component with the `` element. In a small app they might look something like this: diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index d9acf76e15..861f454280 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -110,6 +110,21 @@ What we've built here is a very common type of plugin. It's a top-level tool tha We have also provided external access to our route reference by passing it to the plugin `routes` option. This makes it possible for app integrators to bind an external link from a different plugin to our plugin page. You can read more about how this works in the [External Route References](../architecture/36-routes.md#external-route-references) section. +## Running a dev server + +Each frontend plugin package has a `dev/` folder that is used as the entry point when you run `yarn start`. This is a convenient way to run your plugin in isolation during development. The `@backstage/frontend-dev-utils` package provides a `createDevApp` helper that sets up a minimal app for this purpose: + +```tsx title="in dev/index.ts" +import { createDevApp } from '@backstage/frontend-dev-utils'; +import myPlugin from '../src'; + +createDevApp({ features: [myPlugin] }); +``` + +This will create and render a Backstage app with only your plugin installed. If you need to include additional features that your plugin depends on, pass them along in the `features` array. You can also use `bindRoutes` to wire up any external routes that your plugin depends on. + +The dev setup is started by running `yarn start` in the plugin directory, which uses the `backstage-cli package start` command. It sets up a local development server with hot reloading, just like a full app. + ## Utility APIs Another type of extensions that is commonly used are [Utility APIs](../utility-apis/01-index.md). They can encapsulate shared pieces of functionality of your plugin, for example an API client for a backend service. You can optionally export your Utility API for other plugins to use, or allow integrators to replace the implementation of your Utility API with their own. For details on how to define and provide your own Utility API in your plugin, see the section on [creating Utility APIs](../utility-apis/02-creating.md). diff --git a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md index 3bb613848f..5a3fb530a4 100644 --- a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md +++ b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md @@ -15,13 +15,23 @@ These are the [extension blueprints](../architecture/23-extension-blueprints.md) An API extension is used to add or override [Utility API factories](../utility-apis/01-index.md) in the app. They are commonly used by plugins for both internal and shared APIs. There are also many built-in Api extensions provided by the framework that you are able to override. -### NavItem - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavItemBlueprint.html) +### NavItem (deprecated) - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavItemBlueprint.html) -Navigation item extensions are used to provide menu items that link to different parts of the app. By default nav items are attached to the app nav extension, which by default is rendered as the left sidebar in the app. +The `NavItemBlueprint` is deprecated. The app now auto-discovers navigation items from page extensions, so explicit nav item extensions are no longer needed. To migrate, ensure your plugin and/or page extensions have a `title` and `icon` set — these are used to populate the sidebar automatically. ### Page - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.PageBlueprint.html) -Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes. +Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes. Pages automatically inherit the plugin's `title` and `icon` as defaults, which can be overridden per-page via `PageBlueprint` params. + +To enable sub-pages on a page, you can either omit the `loader` param to use the built-in default implementation that renders sub-pages as tabs, or provide a custom `loader` that explicitly handles the sub-page inputs. + +### SubPage - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.SubPageBlueprint.html) + +Sub-page extensions create tabbed content within a parent page. They are attached to a page extension's `pages` input and rendered as tabs in the page header. Each sub-page has a `path` (relative to the parent page), a `title` for the tab, and an optional `icon`. Content is lazy-loaded via a `loader` function. + +### PluginHeaderAction - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.PluginHeaderActionBlueprint.html) + +Plugin header action extensions provide plugin-scoped actions that appear in the page header. They are automatically scoped to the plugin that provides them and will appear in the header of all pages belonging to that plugin. Actions are lazy-loaded via a `loader` function that returns a React element. ## Extension blueprints in `@backstage/frontend-plugin-api/alpha` @@ -51,10 +61,12 @@ Icon bundle extensions provide the ability to replace or provide new icons to th Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages. -### NavContent - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavContentBlueprint.html) +### NavContent - [Reference](https://backstage.io/api/stable/variables/_backstage_plugin-app-react.NavContentBlueprint.html) Nav content extensions allow you to replace the entire navbar with your own component. They are always attached to the app nav extension. +Your custom component receives a `navItems` prop—a collection with `take(id)` and `rest()` methods for placing specific items in custom positions. Nav items are auto-discovered from page extensions, and metadata (title, icon) comes from page config, nav item extensions, or plugin defaults. Use `navItems.take('page:home')` to take a specific item by extension ID, and `navItems.rest()` to get all remaining items. The deprecated `items` prop (a flat list) remains supported for backward compatibility. + ### Router - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.RouterBlueprint.html) Router extensions allow you to replace the router component used by the app. They are always attached to the app root extension. diff --git a/docs/frontend-system/building-plugins/04-built-in-data-refs.md b/docs/frontend-system/building-plugins/04-built-in-data-refs.md index c01520d7bd..cb16c4975c 100644 --- a/docs/frontend-system/building-plugins/04-built-in-data-refs.md +++ b/docs/frontend-system/building-plugins/04-built-in-data-refs.md @@ -42,6 +42,14 @@ const examplePage = createExtension({ The `title` data reference can be used for defining the extension input/output of string titles. +### `icon` + +| id | type | +| :---------: | :-----------: | +| `core.icon` | `IconElement` | + +The `icon` data reference can be used for defining the extension input/output of icon elements. The type is `IconElement` (`JSX.Element | null`) from `@backstage/frontend-plugin-api`. Icons should be exactly 24x24 pixels in size. + ### `routePath` | id | type | diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index a6ef95ae73..7f85e173c9 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -106,6 +106,8 @@ When filling these out, you have 2 choices, If you opt for the second option of replacing the entire string, take care to not commit your `app-config.yaml` to source control. It may contain passwords that you don't want leaked. +::: + ## Passwordless PostgreSQL in the Cloud If you want to host your PostgreSQL server in the cloud with passwordless authentication, you can use Azure Database for PostgreSQL with Microsoft Entra authentication or Google Cloud SQL for PostgreSQL with Cloud IAM. @@ -188,8 +190,6 @@ backend: # highlight-remove-end ``` -::: - [Start the Backstage app](../index.md#2-run-the-backstage-app): ```shell diff --git a/docs/getting-started/create-a-component.md b/docs/getting-started/create-a-component.md index 29ba99db83..b4e5c6ab19 100644 --- a/docs/getting-started/create-a-component.md +++ b/docs/getting-started/create-a-component.md @@ -6,38 +6,92 @@ description: Leverage the scaffolder to start creating components with best prac Audience: Developers -## Summary +## Overview -This guide will walk you through how to use Software Templates to create new components with baked in best practices. +Components can be created in the Software Catalog using software templates. Templates load skeletons of code, which can include some variables, and incorporate your company's best practices. The templates are published to a location, such as GitHub or GitLab. + +The standalone Backstage application includes the `Example Node.js Template`, which is an example template for the scaffolder that creates and registers a simple Node.js service. You can also [create your own templates](../features/software-templates/adding-templates.md). ## Prerequisites -:::note +For this example, the default Node.js template will be used. The template creates a repository in GitHub and adds the necessary files to it so that the component is integrated into the Software Catalog. Because you are creating a repository, you must first create an integration between Backstage and GitHub. -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=--no-node-snapshot` +- You should have already [installed a standalone app](./index.md). -::: +- Register the [GitHub Scaffolder Action module](../features/software-templates/builtin-actions.md#installing-action-modules). -You should already have [a standalone app](./index.md). +- [Set up a GitHub Integration](../getting-started/config/authentication.md#setting-up-a-github-integration) with Backstage, using a GitHub Personal Access Token. -You will also need to register the [GitHub Scaffolder Action module](../features/software-templates/builtin-actions.md#installing-action-modules) before moving forward. +## Creating the component -## Creating your component +To create the component: -- Go to `create` and choose to create a website with the `Example Node.js Template` -- Type in a name, let's use `tutorial` and click `Next Step` +1. Select `Create`. -![Software template deployment input screen asking for a name](../assets/getting-started/b-scaffold-1.png) + ![select create for new component](../assets/uiguide/select-create-for-new-component.png) -- You should see the following screen: +2. Select `Service` in the `CATEGORIES` dropdown list. +3. Select the `Owner`. For this example, you can select `guest`. +4. Select `Choose` in the `Example Node.js Template`. -![Software template deployment input screen asking for the GitHub username, and name of the new repo to create](../assets/getting-started/b-scaffold-2.png) + ![setup-nodejs-categories-owners](../assets/uiguide/setup-nodejs-cat-owner.png) -- For host, it should default to github.com -- As owner, type your GitHub username -- For the repository name, type `tutorial`. Go to the next step +5. For this example, enter `tutorial` for the `Name` of the service and select `NEXT`. -- Review the details of this new service, and press `Create` if you want to - deploy it like this. -- You can follow along with the progress, and as soon as every step is - finished, you can take a look at your new service + ![enter name of new component](../assets/uiguide/enter-name-of-new-component.png) + +6. Enter your GitHub user name as the `Owner`. +7. Enter `tutorial` for the `Repository` and select `REVIEW`. + + ![select review create component](../assets/uiguide/select-review-create-component.png) + +8. Review the information and select `CREATE`. + + ![select create to make component](../assets/uiguide/select-create-to-make-component.png) + +If you see an error message, similar to the following, + + ![error creating new component](../assets/uiguide/error-creating-new-component.png) + +Perform the following steps: + +1. Close the Backstage app. +2. Enter `CTRL-C` in the terminal window to stop the Backstage frontend and backend. +3. In the terminal window, enter: + + ``` + export NODE_OPTIONS=--no-node-snapshot + ``` + + > **NOTE:** + > The [no-node-snapshot](../features/software-templates/index.md#prerequisites) `NODE_OPTIONS` environment variable is required in order to use the templates. + +4. Enter `yarn start` to restart the Backstage application. +5. Repeat steps to create the component. + +Otherwise, you can follow along with the progress, and as soon as every step is finished, you can take a look at your new service in either the repository or the Catalog. + +![run of example of create component](../assets/uiguide/successful-create-new-component.png) + +Selecting `REPOSITORY` displays the `catalog-info.yaml`file and other project setup files that were created for the new component in the main branch of the `tutorial` repository. + +The `catalog-info.yaml` file describes the entity for the Software Catalog. [Descriptor Format of Catalog Entities](../features/software-catalog/descriptor-format.md) provides additional information. + +``` + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: "tutorial" + spec: + type: service + owner: user:guest + lifecycle: experimental +``` + +Selecting `OPEN IN CATALOG` displays details of the new component, such as its relationships, links, and subcomponents. + +![select open in catalog](../assets/uiguide/tutorial-component-open-in-catalog.png) + +Selecting `Home` in the sidebar, displays the new `tutorial` component in the Catalog. + +![new tutorial component in software catalog](../assets/uiguide/new-tutorial-component-in-software-catalog.png) diff --git a/docs/getting-started/filter-catalog.md b/docs/getting-started/filter-catalog.md new file mode 100644 index 0000000000..3e0de8cdeb --- /dev/null +++ b/docs/getting-started/filter-catalog.md @@ -0,0 +1,57 @@ +--- +id: filter-catalog +title: Filtering the Catalog +description: Filtering the Catalog. +--- + +Audience: All + +## Overview + +The Catalog can be filtered by any combination of owner, kind, type, lifecycle, processing status, namespace, and name. [Customize Filters](../features/software-catalog/catalog-customization.md#customize-filters) provides information on how to modify the available filter criteria. + +![Catalog filter options](../assets/uiguide/catalog-filter-options.png) + +The [Technical Overview](../overview/technical-overview.md#software-catalog-system-model) provides a description of the types of entities displayed in the Catalog. + +## Filtering the Catalog + +You can filter the Catalog using a combination of the following: + +- **Filter by name** + + Enter one or more consecutive letters into the `Filter` field. As you type the letters, the entities whose names do not contain that string will be filtered out of the displayed list. + + ![Filter catalog by name](../assets/uiguide/filter-by-name.png) + +- **Filter by kind** + + Use the `Kind` dropdown list to select which kind of entity to show in the list: + + - API + - Component + - Group + - Location + - System + - Template + - User + +- **Filter by Type** + + Use the `Type` dropdown list to select which type of entity to show in the list. The selections available in the dropdown list depend on the kind of entity selected in the `Kind` list, and the types of entity you have registered for that kind. + +- **Filter by Owner** + + Use the `Owner` dropdown to filter the Catalog list by who owns the entity. + +- **Filter by Lifecycle** + + Use the `Lifecycle` dropdown to filter the Catalog list by lifecycle. + +- **Filter by Processing Status** + + Use the `Processing Status` dropdown to restrict the displayed list to only include those entities which are [orphaned](../features/software-catalog/life-of-an-entity.md#orphaning) or [in error](../features/software-catalog/life-of-an-entity.md#errors). + +- **Filter by Namespace** + + Use the `Namespace` dropdown to filter the catalog list by namespace associated with the entity. diff --git a/docs/getting-started/homepage--old.md b/docs/getting-started/homepage--old.md new file mode 100644 index 0000000000..98021d76a2 --- /dev/null +++ b/docs/getting-started/homepage--old.md @@ -0,0 +1,201 @@ +--- +id: homepage--old +title: Backstage homepage - Setup and Customization (Old Frontend System) +description: Documentation on setting up and customizing Backstage homepage +--- + +::::info +This documentation is for Backstage apps that still use the old frontend +system. If your app uses the new frontend system, read the +[current homepage guide](./homepage.md) instead. +:::: + +## Homepage + +Having a good Backstage homepage can significantly improve the discoverability +of the platform. You want your users to find all the things they need right +from the homepage and never have to remember direct URLs in Backstage. The +[Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) +introduces a system for composing a homepage for Backstage in order to surface +relevant info and provide convenient shortcuts for common tasks. It's designed +with composability in mind with an open ecosystem that allows anyone to +contribute with any component, to be included in any homepage. + +For App Integrators, the system is designed to be composable to give total +freedom in designing a Homepage that suits the needs of the organization. From +the perspective of a Component Developer who wishes to contribute with building +blocks to be included in Homepages, there's a convenient interface for bundling +the different parts and exporting them with both error boundary and lazy +loading handled under the surface. + +At the end of this tutorial, you can expect: + +- Your Backstage app to have a dedicated homepage instead of Software Catalog. +- Understand the composability of homepage and how to start customizing it for + your own organization. + +### Prerequisites + +Before we begin, make sure + +- You have created your own standalone Backstage app using + [`@backstage/create-app`](./index.md#1-create-your-backstage-app) and not + using a fork of the [backstage](https://github.com/backstage/backstage) + repository. +- You do not have an existing homepage, and by default you are redirected to + Software Catalog when you open Backstage. + +Now, let's get started by installing the home plugin and creating a simple +homepage for your Backstage app. + +## Setup + +### 1. Install the plugin + +```bash title="From your Backstage root directory" +yarn --cwd packages/app add @backstage/plugin-home +``` + +### 2. Create a new HomePage component + +Inside your `packages/app` directory, create a new file where our new homepage +component is going to live. Create `packages/app/src/components/home/HomePage.tsx` +with the following initial code + +```tsx +export const HomePage = () => ( + /* We will shortly compose a pretty homepage here. */ +

Welcome to Backstage!

+); +``` + +### 3. Update router for the root `/` route + +If you don't have a homepage already, most likely you have a redirect setup to +use the Catalog homepage as a homepage. + +Inside your `packages/app/src/App.tsx`, look for + +```tsx title="packages/app/src/App.tsx" +const routes = ( + + + {/* ... */} + +); +``` + +Let's replace the `` line and use the new component we created in the +previous step as the new homepage. + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-start */ +import { HomepageCompositionRoot } from '@backstage/plugin-home'; +import { HomePage } from './components/home/HomePage'; +/* highlight-add-end */ + +const routes = ( + + {/* highlight-remove-next-line */} + + {/* highlight-add-start */} + }> + + + {/* highlight-add-end */} + {/* ... */} + +); +``` + +### 4. Update sidebar items + +Let's update the route for "Home" in the Backstage sidebar to point to the new +homepage. We'll also add a Sidebar item to quickly open Catalog. + +| Before | After | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| ![Sidebar without Catalog](../assets/getting-started/sidebar-without-catalog.png) | ![Sidebar with Catalog](../assets/getting-started/sidebar-with-catalog.png) | + +The code for the Backstage sidebar is most likely inside your +[`packages/app-legacy/src/components/Root/Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app-legacy/src/components/Root/Root.tsx). + +Let's make the following changes + +```tsx title="packages/app/src/components/Root/Root.tsx" +/* highlight-add-next-line */ +import CategoryIcon from '@material-ui/icons/Category'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + {/* ... */} + }> + {/* Global nav, not org-specific */} + {/* highlight-remove-next-line */} + + {/* highlight-add-start */} + + + {/* highlight-add-end */} + + + + + {/* End global nav */} + + {/* ... */} + + + +); +``` + +That's it! You should now have _(although slightly boring)_ a homepage! + + + +![Screenshot of a blank homepage](../assets/getting-started/simple-homepage.png) + +In the next steps, we will make it interesting and useful! + +#### Use the default template + +There is a default homepage template +([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) +which we will use to set up our homepage. Checkout the +[blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) +about the Backstage homepage templates for more information. + + + +#### Composing your homepage + +Composing a homepage is no different from creating a regular React Component, +i.e. the App Integrator is free to include whatever content they like. However, +there are components developed with the homepage in mind. If you are looking +for components to use when composing your homepage, you can take a look at the +[collection of Homepage components](https://backstage.io/storybook?path=/story/plugins-home-components) +in storybook. If you don't find a component that suits your needs but want to +contribute, check the +[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). + +> If you want to use one of the available homepage templates you can find the +> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) +> in the storybook under the "Home" plugin. And if you would like to contribute +> a template, please see the +> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) + +```tsx +import Grid from '@material-ui/core/Grid'; +import { HomePageCompanyLogo } from '@backstage/plugin-home'; + +export const HomePage = () => ( + + + + + +); +``` diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 05e422610a..22761eb6ed 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -4,6 +4,13 @@ title: Backstage homepage - Setup and Customization description: Documentation on setting up and customizing Backstage homepage --- +::::info +This documentation is written for the new frontend system, which is the default +in new Backstage apps. If your Backstage app still uses the old frontend system, +read the [old frontend system version of this guide](./homepage--old.md) +instead. +:::: + ## Homepage Having a good Backstage homepage can significantly improve the discoverability of the platform. You want your users to find all the things they need right from the homepage and never have to remember direct URLs in Backstage. The [Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) introduces a system for composing a homepage for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any homepage. @@ -24,39 +31,17 @@ Before we begin, make sure Now, let's get started by installing the home plugin and creating a simple homepage for your Backstage app. -## Setup Methods +## Setup -There are two ways to set up the home plugin, depending on which frontend system your Backstage app uses: - -1. **New Frontend System (Recommended)** - For apps using the new plugin system with extensions and blueprints -2. **Legacy Frontend System** - For existing apps using the legacy plugin architecture - -### New Frontend System Setup - -If your Backstage app uses the [new frontend system](../frontend-system/index.md), follow these steps: - -#### 1. Install the plugin +### 1. Install the plugin ```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-home ``` -#### 2. Add the plugin to your app configuration +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](../frontend-system/building-apps/05-installing-plugins.md). -Update your `packages/app/src/app.tsx` to include the home plugin: - -```tsx title="packages/app/src/app.tsx" -import homePlugin from '@backstage/plugin-home/alpha'; - -const app = createApp({ - features: [ - // ... other plugins - homePlugin, - ], -}); -``` - -#### 3. Configure the homepage as your root route +### 2. Configure the homepage as your root route By default, the homepage will be available at `/home`. To make it your app's landing page at `/`, add this configuration to your `app-config.yaml`: @@ -70,7 +55,7 @@ app: The plugin will automatically add a "Home" navigation item to your sidebar and provide a basic homepage layout. -#### 4. Optional: Enable visit tracking +### 3. Optional: Enable visit tracking Visit tracking is an optional feature that allows users to see their recently visited and most visited pages on the homepage. This feature is **disabled by default** to give you control over what data is collected and stored. @@ -88,156 +73,12 @@ app: - app-root-element:home/visit-listener: true ``` -#### 5. Customizing your homepage +### 4. Customizing your homepage -The New Frontend System provides powerful customization options: +The home plugin provides powerful customization options: **Custom Homepage Layouts**: Use the `HomePageLayoutBlueprint` from `@backstage/plugin-home-react/alpha` to create custom homepage layouts with your own design and widget arrangements. A layout receives the installed widgets and is responsible for rendering them. If no custom layout is installed, the plugin provides a built-in default. **Adding Homepage Widgets**: Register custom widgets using the `HomePageWidgetBlueprint` from the `@backstage/plugin-home-react/alpha` package. For detailed instructions on creating custom layouts, registering widgets, and advanced configuration options, see the [Home plugin documentation](https://github.com/backstage/backstage/tree/master/plugins/home#readme). - -### Legacy Frontend System Setup - -If your Backstage app uses the legacy frontend system, follow these steps: - -#### 1. Install the plugin - -```bash title="From your Backstage root directory" -yarn --cwd packages/app add @backstage/plugin-home -``` - -#### 2. Create a new HomePage component - -Inside your `packages/app` directory, create a new file where our new homepage component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code - -```tsx -export const HomePage = () => ( - /* We will shortly compose a pretty homepage here. */ -

Welcome to Backstage!

-); -``` - -#### 3. Update router for the root `/` route - -If you don't have a homepage already, most likely you have a redirect setup to use the Catalog homepage as a homepage. - -Inside your `packages/app/src/App.tsx`, look for - -```tsx title="packages/app/src/App.tsx" -const routes = ( - - - {/* ... */} - -); -``` - -Let's replace the `` line and use the new component we created in the previous step as the new homepage. - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-start */ -import { HomepageCompositionRoot } from '@backstage/plugin-home'; -import { HomePage } from './components/home/HomePage'; -/* highlight-add-end */ - -const routes = ( - - {/* highlight-remove-next-line */} - - {/* highlight-add-start */} - }> - - - {/* highlight-add-end */} - {/* ... */} - -); -``` - -#### 4. Update sidebar items - -Let's update the route for "Home" in the Backstage sidebar to point to the new homepage. We'll also add a Sidebar item to quickly open Catalog. - -| Before | After | -| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| ![Sidebar without Catalog](../assets/getting-started/sidebar-without-catalog.png) | ![Sidebar with Catalog](../assets/getting-started/sidebar-with-catalog.png) | - -The code for the Backstage sidebar is most likely inside your [`packages/app-legacy/src/components/Root/Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app-legacy/src/components/Root/Root.tsx). - -Let's make the following changes - -```tsx title="packages/app/src/components/Root/Root.tsx" -/* highlight-add-next-line */ -import CategoryIcon from '@material-ui/icons/Category'; - -export const Root = ({ children }: PropsWithChildren<{}>) => ( - - - - {/* ... */} - }> - {/* Global nav, not org-specific */} - {/* highlight-remove-next-line */} - - {/* highlight-add-start */} - - - {/* highlight-add-end */} - - - - - {/* End global nav */} - - {/* ... */} - - - -); -``` - -That's it! You should now have _(although slightly boring)_ a homepage! - - - -![Screenshot of a blank homepage](../assets/getting-started/simple-homepage.png) - -In the next steps, we will make it interesting and useful! - -### Use the default template - -There is a default homepage template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our homepage. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage homepage templates for more information. - - - -### Composing your homepage - -Composing a homepage is no different from creating a regular React Component, -i.e. the App Integrator is free to include whatever content they like. However, -there are components developed with the homepage in mind. If you are looking -for components to use when composing your homepage, you can take a look at the -[collection of Homepage components](https://backstage.io/storybook?path=/story/plugins-home-components) -in storybook. If you don't find a component that suits your needs but want to -contribute, check the -[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). - -> If you want to use one of the available homepage templates you can find the -> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) -> in the storybook under the "Home" plugin. And if you would like to contribute -> a template, please see the -> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) - -```tsx -import Grid from '@material-ui/core/Grid'; -import { HomePageCompanyLogo } from '@backstage/plugin-home'; - -export const HomePage = () => ( - - - - - -); -``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index d7c4ee96c8..5893877a55 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -184,8 +184,14 @@ Choose the correct next steps for your user role, if you're likely to be deployi - Using your Backstage instance - [Logging into Backstage](./logging-in.md) + - [Viewing the Catalog](./viewing-catalog.md) + - [Viewing what you own](./view-what-you-own.md) + - [Viewing entity relationships](./viewing-entity-relationships.md) + - [Filtering the Catalog](./filter-catalog.md) - [Register a component](./register-a-component.md) - [Create a new component](./create-a-component.md) + - [Update a component](./update-a-component.md) + - [Unregistering and deleting a component](./unregister-delete-component.md) Share your experiences, comments, or suggestions with us: [on discord](https://discord.gg/backstage-687207715902193673), file issues for any diff --git a/docs/getting-started/register-a-component.md b/docs/getting-started/register-a-component.md index 4e2076ee8d..1f2fda2871 100644 --- a/docs/getting-started/register-a-component.md +++ b/docs/getting-started/register-a-component.md @@ -8,38 +8,53 @@ Audience: Developers :::note Note Entity files are stored in YAML format, if you are not familiar with YAML, you can learn more about it [here](https://yaml.org). + +[Descriptor Format of Catalog Entities](../features/software-catalog/descriptor-format.md) provides additional information on the format of the YAML entity files. ::: -## Summary +## Overview This guide will walk you through how to pull Backstage data from other locations manually. There are integrations that will automatically do this for you. +When registering a component, you can: + +- Link to an existing entity file: The file is analyzed to determine which entities are defined, and the entities are added to the Scaffolded Backstage App Catalog. For example, `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. + +- Link to a repository: All `catalog-info.yaml` files are discovered in the repository and their defined entities are added to the Scaffolded Backstage App Catalog. For example, `https://github.com/backstage/backstage`. + + :::note Note + If no entities are found, a Pull Request is created that adds an example `catalog-info.yaml` file to the repository. When the Pull Request is merged, the Scaffolded Backstage App Catalog loads all of the defined entities. + + ::: + ## Prerequisites -You should have already [have a standalone app](./index.md). +- You should have already [installed a standalone app](./index.md). -## 1. Finding our template +## Registering a component -Register a new component, by going to `create` and choose `Register existing component` +To manually register a component in the Software Catalog: - +1. Select `Create`. +2. Select `REGISTER EXISTING COMPONENT`. -![Software template main screen, with a blue button to add an existing component](../assets/getting-started/b-existing-1.png) + ![Select Register existing component.](../assets/uiguide/select-register-existing-component.png) -## 2. Filling out the template +3. Fill out the template. -For repository URL, use `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. This is used in our [demo site](https://demo.backstage.io) catalog. + The standalone Backstage application includes one template. For this example, enter the repository URL to the entity file, `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. This is used in the Backstage [demo site](https://demo.backstage.io) catalog. -![Register a new component wizard, asking for an URL to the existing component YAML file](../assets/getting-started/b-existing-2.png) + ![enter url of component entity file.](../assets/uiguide/enter-url-of-component.png) -Hit `Analyze` and review the changes. +4. Select `ANALYZE`. +5. If the changes from `ANALYZE` are correct, select `IMPORT`. -## 3. Import the entity + ![review and select import.](../assets/uiguide/review-select-import.png) -If the changes from `Analyze` are correct, click `Apply`. + If your entity was successfully imported, the details will be displayed. -![Register a new component wizard, showing the metadata for the component YAML we use in this tutorial](../assets/getting-started/b-existing-3.png) + ![details of registered component.](../assets/uiguide/details-for-registered-backstage-website.png) -You should receive a message that your entities have been added. +6. Select `Home` to view your new entity in the Software Catalog. -If you go back to `Home`, you should be able to find `backstage`. You can click it and see the details for this new entity. + ![Backstage website component in software catalog.](../assets/uiguide/backstage-website-registered-catalog-view.png) diff --git a/docs/getting-started/unregister-delete-component.md b/docs/getting-started/unregister-delete-component.md new file mode 100644 index 0000000000..f8034bf3ed --- /dev/null +++ b/docs/getting-started/unregister-delete-component.md @@ -0,0 +1,66 @@ +--- +id: unregister-delete-component +title: Unregistering and deleting a component +description: Unregistering and deleting a component from the catalog +--- + +Audience: Developers + +## Overview + +URLs to YAML files that you registered either using the `Create` button or by adding to your app-config file are both handled by entity providers. + +[Implicit deletion](../features/software-catalog/life-of-an-entity.md#implicit-deletion) occurs when an entity provider issues a deletion of an entity. That entity, as well as the entire tree of entities processed out of it are considered for immediate deletion. + +However, you are also able to manually unregister an entity from the Catalog or perform a direct, [explicit deletion](../features/software-catalog/life-of-an-entity.md#explicit-deletion) of individual entities. + +## Unregistering an entity + +You can unregister an entity so it will not be displayed in the Catalog but still keep its `catalog-info.yaml` file in the repository. This provides the ability to register the entity with the Catalog again in the future. + +To unregister an entity: + +1. In the Catalog, select the entity you want to unregister. In this example, `mytutorial` is being unregistered. + +2. Select the three dots. + +3. Select `Unregister entity` in the dropdown menu. + + ![Screenshot of selecting unregister entity.](../assets/uiguide/select-unregister-entity-from-three-dots.png) + +4. Select `UNREGISTER LOCATION`. The entity is removed from the Catalog. + + ![Screenshot of confirming unregister entity.](../assets/uiguide/confirm-unregister-entity.png) + +## Deleting an entity + +You can also delete an entity from the Catalog. However, this requires that you also delete the `catalog-info.yaml` entity definition file associated with the entity. + +To delete an entity: + +1. Delete the following entity definition files for the entity in the repository: + + - catalog-info.yaml + - index.js + - package.json + +2. In the Backstage App Catalog view, select the entity being deleted. In this example, `mytutorial` is being deleted. + + Since you have deleted the entity definition files, an error is displayed that states the `catalog-info.yaml` file cannot be found. + + ![Screenshot of error finding catalog info yaml file.](../assets/uiguide/error-message-catalog-info-file-deleted.png) + +3. Select the three dots. +4. Select `Unregister entity` in the dropdown menu. + + ![Screenshot of selecting unregister entity.](../assets/uiguide/select-unregister-entity-from-three-dots.png) + +5. Select `ADVANCED OPTIONS`. + + ![Screenshot of selecting advanced options.](../assets/uiguide/select-advanced-options.png) + +6. Select `DELETE ENTITY`. + + ![Screenshot of selecting delete entity.](../assets/uiguide/select-delete-entity.png) + +A confirmation message that the entity has been successfully deleted is briefly displayed. The entity is no longer displayed in the Catalog. diff --git a/docs/getting-started/update-a-component.md b/docs/getting-started/update-a-component.md new file mode 100644 index 0000000000..ffe21bc8b3 --- /dev/null +++ b/docs/getting-started/update-a-component.md @@ -0,0 +1,33 @@ +--- +id: update-a-component +title: Update a Component +description: Update an existing component. +--- + +Audience: Developers + +## Overview + +Components in the Software Catalog are created using a software template. The template generates a `catalog-info.yaml` file in either GitHub or GitLab that defines the entity. To update the component, you must edit its corresponding `catalog-info.yaml` entity definition file. + +## Updating the component + +To update a component using the Backstage UI: + +1. Select the "Edit" icon associated with the component. In this example, the `tutorial` entity is selected. + + ![select edit icon for component](../assets/uiguide/select-edit-icon-for-component.png) + + The associated `catalog-info.yaml` file is displayed. + + ![tutorial component catalog-info.yaml file](../assets/uiguide/tutorial-catalog-info-yaml-file.png) + +2. Make your changes to the YAML file. In this example, the name of the component is changed to `mytutorial`. + + ![component name updated](../assets/uiguide/component-name-updated.png) + +3. Select `Commit changes` to commit your changes to the appropriate branch and go through your normal PR review procedure. + +4. Once the updated `catalog-info.yaml` file has been merged into the branch associated with the component, then you will see the updated information in the Software Catalog. + + ![updated component name in ui](../assets/uiguide/updated-component-name-in-ui.png) diff --git a/docs/getting-started/view-what-you-own.md b/docs/getting-started/view-what-you-own.md new file mode 100644 index 0000000000..fc8b1feeae --- /dev/null +++ b/docs/getting-started/view-what-you-own.md @@ -0,0 +1,22 @@ +--- +id: view-what-you-own +title: Viewing what you own +description: View the entities that you own either directly or through a group +--- + +Audience: All + +You can own entities either directly or through a group that you're part of. + +To view the entities that you own: + +1. Select `Home` in the sidebar. +2. Select `User` in the `Kind` dropdown list. +3. Select your username in the `All Users` list. + +A page is displayed that shows the entities of which you have ownership, either directly, or through a group of which you are a member. You can toggle between showing: + +- `Direct Relations` - entities that you directly own +- `Aggregated Relations` - entities that you own through your group + +![Screenshot of the Backstage portal entities owned by guest user.](../assets/uiguide/entities-owned-by-me.png) diff --git a/docs/getting-started/viewing-catalog.md b/docs/getting-started/viewing-catalog.md new file mode 100644 index 0000000000..acfa470cc2 --- /dev/null +++ b/docs/getting-started/viewing-catalog.md @@ -0,0 +1,85 @@ +--- +id: viewing-catalog +title: Viewing the Catalog +sidebar_label: Viewing the Catalog +description: Viewing the Catalog +--- + +Audience: All + +## Overview + +Initially, when you log into your standalone Backstage App, `Home` is selected in the sidebar, which displays the Catalog in the main panel. + +There are four main entities that you should become familiar with: + +- `Components` - Individual pieces of software that can be tracked in source control and can implement APIs for other components to consume. +- `Resources` - The physical or virtual infrastructure needed to operate a component. +- `Systems` - A collection of resources and components that cooperate to perform a function by exposing one or several public APIs. It hides the resources and private APIs between the components from the consumer. +- `Domains` - A collection of systems that share terminology, domain models, metrics, KPIs, business purpose, or documentation. + +The [Technical Overview](../overview/technical-overview.md#software-catalog-system-model) provides a description of all of the types of entities displayed in the Catalog. + +It should be noted that you can also [create your own kinds of entities](../features/software-catalog/extending-the-model.md#adding-a-new-kind), if you need to model something in your organization that does not map to one of the existing entity types. + +Initially, the Catalog displays registered entities matching the following filter settings: + +- `Kind` - Component +- `Type` - all +- `Owner` - Owned +- `Lifecycle` - list of [lifecycle](../features/software-catalog/descriptor-format.md#speclifecycle-required-1) values of entities in the Catalog +- `Processing Status` - normal +- `Namespace` - The ID of a [namespace](../features/software-catalog/descriptor-format.md#namespace-optional) to which the entity belongs + +You can change the initial setting for the [Owner](../features/software-catalog/catalog-customization.md#initially-selected-filter) and [Kind](../features/software-catalog/catalog-customization.md#initially-selected-kind) filters. + +## Informational columns for each entity + +For each kind of entity, a set of columns display information regarding the entity. For example, the default set of information for a `Component` is: + +- `Name` - the name of the component +- `System` - an optional field that references the system to which the component belongs +- `Owner` - the owner of the component +- `Type` - common types are as follows, but you can [create a new type](../features/software-catalog/extending-the-model.md#adding-a-new-type-of-an-existing-kind) to meet your organization's needs + - `service` - a backend service, typically exposing an API + - `website` - a website + - `library` - a software library, such as an npm module or a Java library +- `Lifecycle` + - `experimental` - an experiment or early, non-production component, signaling + that users may not prefer to consume it over other more established + components, or that there are low or no reliability guarantees + - `production` - an established, owned, maintained component + - `deprecated` - a component that is at the end of its lifecycle, and may + disappear at a later point in time +- `Description` - an optional field that describes the component. +- `Tags` - an optional field that can be used for searching +- `Actions` - see [Catalog Actions](#catalog-actions) + +You can modify the columns associated with each kind of entity, following the instructions in [Customize Columns](../features/software-catalog/catalog-customization.md#customize-columns). + +## Catalog Actions + +For each entity, there are a set of actions that are available. + +![Screenshot explaining entity actions.](../assets/uiguide/entity-actions.png) + +From left to right, the actions are: + +- View - View the `catalog-info.yaml` file that defines the entity. +- Edit - Edit the `catalog-info.yaml` file that defines the entity. See [Updating a Component](../getting-started/update-a-component.md) +- Star - Designate the entity as a favorite. You can [filter](../getting-started/filter-catalog.md) the catalog for starred entities. + +[Customize Actions](../features/software-catalog/catalog-customization.md#customize-actions) describes how you can modify the actions that are displayed. + +## Viewing entity details + +Selecting an entity in the main panel displays details of the entity. The type of details depends on the type of entity. For example, selecting a Component, such as `example-website`, displays the following details: + +- `About` - Metadata for the entity, such as description, owner, tags, and domain. +- `Relations` - see [Viewing entity relationships](../getting-started/viewing-entity-relationships.md) +- `Links` - any links associated with the entity +- `Has subcomponents` - An entity reference to another component of which the component is a part + +Selecting a System, such as `examples`, displays `About`, `Relations`, and `Links` similar to a Component, but it also includes `Has components`, `APIs` and `Has Resources`. + +![Details of system entity.](../assets/uiguide/details-system-entity.png) diff --git a/docs/getting-started/viewing-entity-relationships.md b/docs/getting-started/viewing-entity-relationships.md new file mode 100644 index 0000000000..c791ab5bc7 --- /dev/null +++ b/docs/getting-started/viewing-entity-relationships.md @@ -0,0 +1,98 @@ +--- +id: viewing-entity-relationships +title: Viewing entity relationships +description: View the relationships between the entities in the Catalog +--- + +Audience: All + +Each of the entities in the Catalog has various relationships to each other. For example, the demo data includes an API that provides data to a website. `guests` is the owner of the API and the website, and anyone signed in as part of the `guests` group can maintain them. + +To see these relationships: + +1. Select the name of the component in the main panel, in this example, `example-website`. + + ![Screenshot of the Backstage portal with example-website selected.](../assets/uiguide/select-example-website.png) + + A page is displayed that includes a `Relations` section. This section displays the selected entity and any other types of entities to which it is related. Each relationship is also designated, such as `hasPart/partOf` and `apiProvidedBy/providesApi`. [Well-known Relations between Catalog Entities](../features/software-catalog/well-known-relations.md) describes the most common relationships, but you can also [create your own relationships](../features/software-catalog/extending-the-model.md#adding-a-new-relation-type). + +2. Selecting any of the related entities allows you to drill down further through the system model. + + ![Screenshot of the example-website Relations panel.](../assets/uiguide/example-website-relationships.png) + +## Filtering the relationships + +You can also view the relationships in the [Catalog Graph](../features/software-catalog/creating-the-catalog-graph.md). This view allows you to filter what entities and relationships to display. + +To display the Catalog Graph: + +1. Select the name of the component in the main panel, in this example, `example-website`. + + A page is displayed that includes a `Relations` section. + +2. Select `View graph` in the `Relations` section. + + ![Select View graph.](../assets/uiguide/select-view-graph.png) + + The `Catalog Graph` is displayed. + + ![Screenshot of Catalog Graph.](../assets/uiguide/catalog-graph.png) + +### Setting the filters + +The `Catalog Graph` automatically reflects any changes you make to the filter settings. You can set the following filters: + +- `MAX DEPTH` + + - `MAX DEPTH` = 1 + + ![Max Depth at 1](../assets/uiguide/max-depth-1.png) + + - `MAX DEPTH` = infinite + + ![Max Depth at infinite](../assets/uiguide/max-depth-infinite.png) + +- `KINDS` - select what kinds of entities you want to view, default is all kinds +- `RELATIONS` - select which relationships you want to view, default is all relationships +- `Direction` - orientation in which you want to view the entity and its associated nodes + - Top to bottom + - Bottom to top + - Left to right + - Right to left +- `Curve` + + - `Curve` = Monotone + + ![Curve at Monotone.](../assets/uiguide/curve-monotone.png) + + - `Curve` = Step Before + + ![Curve at Step Before.](../assets/uiguide/curve-step-before.png) + +You can also toggle: + +- `Simplified` + - On = simple view + - Off = detailed view +- `Merge relations` + + - On = You see the relationship from the selected entity to the nodes and from the nodes to the selected entity. + - Off = You only see relations from the selected entity to its nodes. + + The following graphics illustrate the view of the nodes and relationships, based on the combination of the settings of `Simplified` and `Merge relations`. + + - `Simplified` = On and `Merge Relations` = On + + ![Simplified is On and Merge Relations is On.](../assets/uiguide/simplify-on-merge-relations-on.png) + + - `Simplified` = On and `Merge Relations` = Off + + ![Simplified is On and Merge Relations is Off.](../assets/uiguide/simplify-on-merge-relations-off.png) + + - `Simplified` = Off and `Merge Relations` = On + + ![Simplified is Off and Merge Relations is On.](../assets/uiguide/simplify-off-merge-relations-on.png) + + - `Simplified` = Off and `Merge Relations` = Off + + ![Simplified is Off and Merge Relations is Off.](../assets/uiguide/simplify-off-merge-relations-off.png) diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index fcba5ca04f..74e2fde6a5 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from an AWS S3 Bucket --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/aws-s3/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The AWS S3 integration has a special entity provider for discovering catalog entities located in an S3 Bucket. If you have a bucket that contains multiple catalog files, and you want to automatically discover them, you can use this diff --git a/docs/integrations/azure-blobStorage/discovery.md b/docs/integrations/azure-blobStorage/discovery.md index bed97441f9..f0019a5c01 100644 --- a/docs/integrations/azure-blobStorage/discovery.md +++ b/docs/integrations/azure-blobStorage/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from an Azure Blob Storage account --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). -::: - The Azure Blob Storage account integration has a special entity provider for discovering catalog entities located in a storage account container. If you have a container that contains multiple catalog files, and you want to automatically discover them, you can use this @@ -65,7 +61,7 @@ the Azure catalog plugin: yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` -Then updated your backend by adding the following line: +Then update your backend by adding the following line: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 7e4d376a7d..67d70c7f6d 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from repositories in an Azure DevOps organization --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/azure/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Azure DevOps integration has a special entity provider for discovering catalog entities within an Azure DevOps. The provider will crawl your Azure DevOps organization and register entities matching the configured path. This can be useful as an alternative to static locations or manually adding things to the catalog. This guide explains how to install and configure the Azure DevOps Entity Provider (recommended) or the Azure DevOps Processor. @@ -101,7 +97,7 @@ the Azure catalog plugin: yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` -Then updated your backend by adding the following line: +Then update your backend by adding the following line: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index efcd8ae612..7bd93f38b3 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -5,10 +5,6 @@ sidebar_label: Org Data description: Importing users and groups from Microsoft Entra ID into Backstage --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/azure/org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Backstage catalog can be set up to ingest organizational data - users and teams - directly from a tenant in Microsoft Entra ID via the Microsoft Graph API. @@ -45,7 +41,7 @@ catalog: For large organizations, this plugin can take a long time, so be careful setting low frequency / timeouts and importing a large amount of users / groups for the first try. ::: -Finally, updated your backend by adding the following line: +Finally, update your backend by adding the following line: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 3f5920be5d..21b896e072 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -39,9 +39,9 @@ package. yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` -### Installation with New Backend System +Then add the following to your backend: -```ts +```ts title="packages/backend/src/index.ts" // optional if you want HTTP endpoints to receive external events // backend.add(import('@backstage/plugin-events-backend')); // optional if you want to use AWS SQS instead of HTTP endpoints to receive external events @@ -63,59 +63,6 @@ Further documentation: - - -### Installation with Legacy Backend System - -Please follow the installation instructions at - -- -- - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) -- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) -- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-start */ -import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud'; -/* highlight-add-end */ - -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - const bitbucketCloudProvider = BitbucketCloudEntityProvider.fromConfig( - env.config, - { - auth: env.auth, - catalogApi: new CatalogClient({ discoveryApi: env.discovery }), - events: env.events, - logger: env.logger, - scheduler: env.scheduler, - }, - ); - builder.addEntityProvider(bitbucketCloudProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - -**Attention:** -`catalogApi` and `tokenManager` are required at this variant -compared to the one without events support. - ## Configuration To use the entity provider, you'll need a [Bitbucket Cloud integration set up](locations.md). diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index bbdd105af4..ee96118f2a 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from repositories in Bitbucket Server --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/bitbucketServer/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Bitbucket Server integration has a special entity provider for discovering catalog files located in Bitbucket Server. The provider will search your Bitbucket Server account and register catalog files matching the configured path @@ -25,9 +21,9 @@ dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to you yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` -### Installation with New Backend System +Then add the following to your backend: -```ts +```ts title="packages/backend/src/index.ts" // optional if you want HTTP endpojnts to receive external events // backend.add(import('@backstage/plugin-events-backend')); // optional if you want to use AWS SQS instead of HTTP endpoints to receive external events diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index 06fb144e50..a9ea80e088 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from Gerrit repositories --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/gerrit/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Gerrit integration has a special entity provider for discovering catalog entities from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get a list of repositories and will automatically ingest all `catalog-info.yaml` files diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 66fdaa503b..c4dbc2e10b 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -5,10 +5,6 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from repositories in a GitHub organization or App --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/github/discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - ## GitHub Provider The GitHub integration has a discovery provider for discovering catalog diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 5321423432..1d5eac1581 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -5,10 +5,6 @@ sidebar_label: Org Data description: Importing users and groups from a GitHub organization into Backstage --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/integrations/github/org--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The Backstage catalog can be set up to ingest organizational data - users and teams - directly from an organization in GitHub or GitHub Enterprise. The result is a hierarchy of diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 4d801bdd77..315cc9a4a7 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -23,8 +23,6 @@ the gitlab catalog plugin: yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab ``` -### Installation with New Backend System - Then add the following to your backend initialization: ```ts title="packages/backend/src/index.ts" @@ -51,87 +49,6 @@ Further documentation: - [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) -### Installation with Legacy Backend System (skip if you are using Backstage v1.31.0 or later) - -#### Installation without Events Support - -Add the segment below to `packages/backend/src/plugins/catalog.ts`: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-start */ - builder.addEntityProvider( - ...GitlabDiscoveryEntityProvider.fromConfig(env.config, { - logger: env.logger, - // optional: alternatively, use scheduler with schedule defined in app-config.yaml - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - // optional: alternatively, use schedule - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - // .. -} -``` - -#### Installation with Events Support - -Please follow the installation instructions at - -- [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) -- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) -- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { GitlabDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); - /* highlight-add-start */ - const gitlabProvider = GitlabDiscoveryEntityProvider.fromConfig(env.config, { - logger: env.logger, - // optional: alternatively, use scheduler with schedule defined in app-config.yaml - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - // optional: alternatively, use schedule - scheduler: env.scheduler, - events: env.events, - }); - builder.addEntityProvider(gitlabProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - ## Configuration To use the discovery provider, you'll need a GitLab integration @@ -140,7 +57,7 @@ to the catalog configuration. :::note Note -If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. +The `schedule` has to be setup in the config, as shown below. ::: diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index bddd1c13a1..6c5641924d 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -29,8 +29,6 @@ As this provider is not one of the default providers, you will first need to ins yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab @backstage/plugin-catalog-backend-module-gitlab-org ``` -### Installation with New Backend System - Then add the following to your backend initialization: ```ts title="packages/backend/src/index.ts @@ -58,92 +56,6 @@ Further documentation: - [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) -### Installation with Legacy Backend System - -#### Installation without Events Support - -Add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: - -```ts -/* packages/backend/src/plugins/catalog.ts */ -/* highlight-add-next-line */ -import { GitlabOrgDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /** ... other processors and/or providers ... */ - /* highlight-add-start */ - builder.addEntityProvider( - ...GitlabOrgDiscoveryEntityProvider.fromConfig(env.config, { - logger: env.logger, - // optional: alternatively, use scheduler with schedule defined in app-config.yaml - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - // optional: alternatively, use schedule - scheduler: env.scheduler, - }), - ); - /* highlight-add-end */ - // .. -} -``` - -#### Installation with Events Support - -Please follow the installation instructions at - -- [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) - -Additionally, you need to decide how you want to receive events from external sources like - -- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) -- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) -- [via Google Pub/Sub](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-google-pubsub/README.md) -- [via a Kafka topic](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-kafka/README.md) - -Set up your provider - -```ts title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { GitlabOrgDiscoveryEntityProvider } from '@backstage/plugin-catalog-backend-module-gitlab'; -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - builder.addProcessor(new ScaffolderEntitiesProcessor()); - /* highlight-add-start */ - const gitlabOrgProvider = GitlabOrgDiscoveryEntityProvider.fromConfig( - env.config, - { - logger: env.logger, - // optional: alternatively, use scheduler with schedule defined in app-config.yaml - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 30 }, - timeout: { minutes: 3 }, - }), - // optional: alternatively, use schedule - scheduler: env.scheduler, - events: env.events, - }, - ); - builder.addEntityProvider(gitlabOrgProvider); - /* highlight-add-end */ - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - return router; -} -``` - ## Configuration To use the entity provider, you'll need a [Gitlab integration set up](https://backstage.io/docs/integrations/gitlab/locations). @@ -163,7 +75,7 @@ will be those visible to the account which provisioned the token. :::note Note -If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. +The `schedule` has to be setup in the config, as shown below. ::: diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 8adc81184a..22a843009f 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -40,7 +40,7 @@ catalog: timeout: PT15M ``` -Finally, updated your backend by adding the following line: +Finally, update your backend by adding the following line: ```ts title="packages/backend/src/index.ts" backend.add(import('@backstage/plugin-catalog-backend')); diff --git a/docs/integrations/okta/org.md b/docs/integrations/okta/org.md new file mode 100644 index 0000000000..63769e7c85 --- /dev/null +++ b/docs/integrations/okta/org.md @@ -0,0 +1,23 @@ +--- +id: org +title: Okta Organizational Data +sidebar_label: Org Data +description: Ingesting organizational data from Okta into Backstage +--- + +The Backstage catalog can be set up to ingest organizational data — users and +groups — directly from Okta. The result is a hierarchy of +[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and +[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind +entities that mirror your Okta organization. + +This integration is provided by the community-maintained +[`@roadiehq/catalog-backend-module-okta`](https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/backend/catalog-backend-module-okta) +plugin, owned and maintained by [Roadie](https://roadie.io/). + +## Installation and configuration + +For setup instructions, including authentication options (API token and OAuth +2.0), user/group filtering, custom naming strategies, and entity transformers, +see the +[plugin documentation maintained by Roadie](https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/backend/catalog-backend-module-okta). diff --git a/docs/landing-page/doc-landing-page.md b/docs/landing-page/doc-landing-page.md index 8697144bd8..b7cc44c696 100644 --- a/docs/landing-page/doc-landing-page.md +++ b/docs/landing-page/doc-landing-page.md @@ -33,13 +33,26 @@ description: Documentation landing page. Configure, Deploy, & Upgrade.

@@ -60,7 +73,7 @@ description: Documentation landing page.
  • Search
  • Software Catalog
  • Software Templates (aka Scaffolder)
  • -
  • TechDocs
  • +
  • TechDocs - a docs-like-code solution
  • @@ -102,7 +115,7 @@ description: Documentation landing page. diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index af0b9f8f93..501678d107 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -156,6 +156,34 @@ notifications: Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send messages to more than one Slack workspace. Org-Wide App installation is not currently supported. +### Customize Slack Message Structure + +You can customize how notifications look in Slack by providing your own message layout through the `notificationsSlackBlockKitExtensionPoint` + +```ts +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { notificationsSlackBlockKitExtensionPoint } from '@backstage/plugin-notifications-backend-module-slack'; + +export const notificationsSlackFormattingModule = createBackendModule({ + pluginId: 'notifications', + moduleId: 'slack-formatting', + register(reg) { + reg.registerInit({ + deps: { + slackBlockKit: notificationsSlackBlockKitExtensionPoint, + }, + async init({ slackBlockKit }) { + slackBlockKit.setBlockKitRenderer(payload => [ + // Custom block kit layout + ]); + }, + }); + }, +}); +``` + +If you do not register a custom renderer, the default renderer is used. + ### Broadcast Channel Routing For more granular control over where broadcast notifications are sent, you can use `broadcastRoutes` to route notifications to different Slack channels based on their origin and/or topic. This is useful when you want different types of notifications to go to different channels. diff --git a/docs/openapi/01-getting-started.md b/docs/openapi/01-getting-started.md index 875a369dbf..c300ac1370 100644 --- a/docs/openapi/01-getting-started.md +++ b/docs/openapi/01-getting-started.md @@ -26,7 +26,14 @@ This tutorial assumes that you're already familiar with the following, 1. How to build a Backstage plugin. 2. `Express.js` and `Typescript` -3. OpenAPI 3.0 schemas +3. OpenAPI 3.1 schemas + +:::note OpenAPI Version Support +Backstage supports both OpenAPI 3.0 and 3.1 specifications. If you have existing OpenAPI 3.0 specs, we recommend that you migrate them to 3.1. The main changes are: + +- Replace `nullable: true` with `type: ['string', 'null']` or use `anyOf`/`oneOf` +- Remove `allowReserved` from path parameters (only valid on query/cookie parameters in 3.1) + ::: ### Setting up diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index d9a03d9668..d1a290e14f 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -55,4 +55,4 @@ guidelines to get started. If you have specific questions about the roadmap, please create an [issue](https://github.com/backstage/backstage/issues/new/choose), ping us on -[Discord](https://discord.gg/backstage-687207715902193673), or [book time](https://info.backstage.spotify.com/office-hours) with the Spotify team. +[Discord](https://discord.gg/backstage-687207715902193673), or [book time](https://spoti.fi/backstageofficehours) with the Spotify team. diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index 6acba915d5..5c33c5f8e6 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -25,12 +25,12 @@ An **external user** is a user that does not belong to the other three groups, f :::info This section assumes that you are using the -[new backend system](../backend-system/index.md) and at least Backstage release [version 1.24](../releases/v1.24.0.md). Before that Backstage did not come with built-in protection against unauthorized access and you were required to deploy it in a protected environment. +[backend system](../backend-system/index.md) and at least Backstage release [version 1.24](../releases/v1.24.0.md). Before that Backstage did not come with built-in protection against unauthorized access and you were required to deploy it in a protected environment. ::: Backstage is primarily designed to be deployed in a protected environment rather than being exposed to the public internet. From a confidentiality and integrity perspective, Backstage is designed to protect against unauthorized access to data and to ensure that data is not tampered with. However, Backstage does not provide more than rudimentary protection against denial of service attacks, and it is the responsibility of the operator to ensure that the Backstage deployment is protected against such attacks. A common and recommended way to protect a Backstage deployment from unauthorized access is to deploy it behind an authenticating proxy such as AWS’s ALB, GCP’s IAP, or Cloudflare Access. -Users that are signed-in in to Backstage generally have full access to all information and actions. If more fine-grained control is required, the [permissions system](../permissions/overview.md) should be enabled and configured to restrict access as necessary. +Users that are signed in to Backstage generally have full access to all information and actions. If more fine-grained control is required, the [permissions system](../permissions/overview.md) should be enabled and configured to restrict access as necessary. An operator is responsible for protecting the integrity of configuration files as it may otherwise be possible to introduce vulnerable configurations, as well as the confidentiality of configured secrets related to Backstage as these typically include authentication details to third party systems. diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index b4ca86adf5..4b188ce7f9 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -4,10 +4,6 @@ title: Defining custom permission rules description: How to define custom permission rules for existing resources --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/custom-rules--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - For some use cases, you may want to define custom [rules](../references/glossary.md#rule-permission-plugin) in addition to the ones provided by a plugin. In the [previous section](./writing-a-policy.md) we used the `isEntityOwner` rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what [system](https://backstage.io/docs/features/software-catalog/system-model#system) an entity is part of. ## Define a custom rule diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index f5b9092ec5..3e40b4597c 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -4,10 +4,6 @@ title: Getting Started description: How to get started with the permission framework as an integrator --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/getting-started--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others. ## Prerequisites diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 743c5c9718..4ed348dae6 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -4,10 +4,6 @@ title: 1. Tutorial setup description: How to get started with the permission framework as a plugin author --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/01-setup--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - The following tutorial is designed to help plugin authors add support for permissions to their plugins. We'll add support for permissions to example `todo-list` and `todo-list-backend` plugins, but the process should be similar for other plugins! The rest of this page is focused on adding the `todo-list` and `todo-list-backend` plugins to your Backstage instance. If you want to add support for permissions to your own plugin instead, feel free to skip to the [next section](./02-adding-a-basic-permission-check.md). diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 8ba3ac3152..3790fc8577 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -4,10 +4,6 @@ title: 2. Adding a basic permission check description: Explains how to add a basic permission check to a Backstage plugin --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), you can use a _basic permission check_. For this kind of check, we simply need to define a permission, and call `authorize` with it. For this tutorial, we'll use a basic permission check to authorize the `create` endpoint in our todo-backend. This will allow Backstage integrators to control whether each of their users is authorized to create todos by adjusting their [permission policy](../../references/glossary.md#policy-permission-plugin). 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 afa56307ce..1e1a73607b 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 @@ -4,10 +4,6 @@ title: 3. Adding a resource permission check description: Explains how to add a resource permission check to a Backstage plugin --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - When performing updates (or other operations) on specific [resources](../../references/glossary.md#resource-permission-plugin), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise. ## Creating the update permission diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 9b79cec042..28ed56608c 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -4,10 +4,6 @@ title: 4. Authorizing access to paginated data description: Explains how to authorize access to paginated data in a Backstage plugin --- -:::info -This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! -::: - Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize access based on the characteristics of each resource. However, we'll need to authorize a list of resources for this endpoint. One possible solution may leverage the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`: diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 2bf2bcade9..652c698be2 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -4,10 +4,6 @@ title: Writing a permission policy description: How to write your own permission policy as a Backstage integrator --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/writing-a-policy--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)! -::: - In the [previous section](./getting-started.md), we were able to set up the permission framework and make a simple change to our `TestPermissionPolicy` to confirm that policy is indeed wired up correctly. That policy looked like this: diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 9b8b63fc89..2872fdcda9 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -38,12 +38,6 @@ cd plugins/carmen-backend yarn start ``` -:::note Note - -This documentation assumes you are using the latest version of Backstage and the new backend system. If you are not, please upgrade and migrate your backend using the [Migration Guide](../backend-system/building-backends/08-migrating.md) - -::: - This will think for a bit, and then say `Listening on :7007`. In a different terminal window, now run diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index e9e000df0d..2d0e34ba4b 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -19,11 +19,18 @@ import { createPlugin } from '@backstage/core-plugin-api'; export const examplePlugin = createPlugin({ // ... - featureFlags: [{ name: 'show-example-feature' }], + featureFlags: [ + { + name: 'show-example-feature', + description: 'Enables the new beta dashboard view', + }, + ], // ... }); ``` +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 `featureFlags` array in the diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 911d74b019..417b932451 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -94,7 +94,7 @@ const systemPage = ( - + diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index fd80e0a1eb..7a7201d9e6 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -4,10 +4,6 @@ title: Integrating Search into a plugin description: How to integrate Search into a Backstage plugin --- -:::info -This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/plugins/integrating-search-into-plugins--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.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 4eb5e864d1..2770f76799 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -1,12 +1,12 @@ --- id: internationalization -title: Internationalization (Experimental) -description: Documentation on adding internationalization to the plugin +title: Internationalization +description: Documentation on adding internationalization to plugins and apps --- ## Overview -The Backstage core function provides internationalization for plugins. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. +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 @@ -183,16 +183,56 @@ return ( 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 overwrite plugin messages +## For an application developer -Step 1: Create translation resources +As an app developer you can both override the default English messages of any plugin, and provide translations for additional languages. -You should separate different translations to their own files and import them in the main file: +### 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/core-plugin-api/alpha'; +import { createTranslationResource } from '@backstage/frontend-plugin-api'; import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; export const userSettingsTranslations = createTranslationResource({ @@ -203,10 +243,12 @@ export const userSettingsTranslations = createTranslationResource({ }); ``` +The translation messages can be defined using `createTranslationMessages` for type safety: + ```ts // packages/app/src/translations/userSettings-zh.ts -import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; const zh = createTranslationMessages({ @@ -221,7 +263,7 @@ const zh = createTranslationMessages({ export default zh; ``` -It's also possible to export the list of messages directly: +Or as a plain object export: ```ts // packages/app/src/translations/userSettings-zh.ts @@ -239,11 +281,7 @@ export default { }; ``` -You should change `zh` under the translations object to your local language. - -Step 2: Config translations in `packages/app/src/App.tsx` - -In an app you can both override the default messages, as well as register translations for additional languages: +Register it with the available languages declared: ```diff + import { userSettingsTranslations } from './translations/userSettings'; @@ -256,6 +294,116 @@ In an app you can both override the default messages, as well as register transl }) ``` -Step 3: Check everything is working correctly +Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly. -Go to `Settings` page, you should see change language buttons just under change theme buttons. And then switch language, you should see language had changed +### 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/plugins/plugin-directory-audit.md b/docs/plugins/plugin-directory-audit.md new file mode 100644 index 0000000000..9e82cb6de5 --- /dev/null +++ b/docs/plugins/plugin-directory-audit.md @@ -0,0 +1,23 @@ +--- +id: plugin-directory-audit +title: Plugin Directory Audit +description: Details about the process for auditing plugins in the directory +--- + +## Audit Process + +We have a simple process in place to audit the plugins in the Plugin Directory: + +1. On a quarterly basis we run the following script: `node ./scripts/plugin-directory-audit.js --audit` +2. This script will flag any plugin as `inactive` that has not been updated on NPM in more than 365 days if it was `active`. +3. It will also flag any plugin that has not been updated on NPM in more than 365 days as `archived` if it was `inactive`. +4. For any plugin flagged as `inactive` or `archived` and has been updated on NPM in less than 365 days they will be flagged as `active` again. +5. These changes will then be submitted as a PR, approved and merged. + +The impact of a plugin being set to `inactive` means they will show in the inactive section at the bottom of the Plugin Directory. A plugin that is set to `archived` will **not** show up in the Plugin Directory at all. + +:::tip + +If your plugin moved to `inactive` or `archived` and you update it, please submit a PR to update the status to `active`! + +::: diff --git a/docs/releases/v1.48.0-changelog.md b/docs/releases/v1.48.0-changelog.md new file mode 100644 index 0000000000..19a21dd4b5 --- /dev/null +++ b/docs/releases/v1.48.0-changelog.md @@ -0,0 +1,3182 @@ +# Release v1.48.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.48.0](https://backstage.github.io/upgrade-helper/?to=1.48.0) + +## @backstage/backend-app-api@1.5.0 + +### Minor Changes + +- f1d29b4: Added support for extension point factories, along with the ability to report module startup failures via the extension point factory context. + +### Patch Changes + +- 6bb2f21: Fixed memory leak by properly cleaning up process event listeners on backend shutdown. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/backend-plugin-api@1.7.0 + +### Minor Changes + +- f1d29b4: Added support for extension point factories. This makes it possible to call `registerExtensionPoint` with a single options argument and provide a factory for the extension point rather than a direct implementation. The factory is passed a context with a `reportModuleStartupFailure` method that makes it possible for plugins to report and attribute startup errors to the module that consumed the extension point. +- bb9b471: Plugin IDs that do not match the standard format are deprecated (letters, digits, and dashes only, starting with a letter). Plugin IDs that do no match this format will be rejected in a future release. + + In addition, plugin IDs that don't match the legacy pattern that also allows underscores, with be rejected. + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/cli-common@0.1.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + +## @backstage/backend-test-utils@1.11.0 + +### Minor Changes + +- 42abfb1: Added `createServiceMock`, a public utility for creating `ServiceMock` instances for custom service refs. This allows plugin authors to define mock creators for their own services following the same pattern as the built-in `mockServices` mocks. + +### Patch Changes + +- f1d29b4: Updated `startTestBackend` to support factory-based extension points (v1.1 format) in addition to the existing direct implementation format. +- 7455dae: Use node prefix on native imports +- 68eb322: Added `@types/jest` as an optional peer dependency, since jest types are exposed in the public API surface. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-app-api@1.5.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/catalog-client@1.13.0 + +### Minor Changes + +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries + +### Patch Changes + +- 9cf6762: Improved the `InMemoryCatalogClient` test utility to support ordering, pagination, full-text search, and field projection for entity query methods. Also fixed `getEntityFacets` to correctly handle multi-valued fields. +- Updated dependencies + - @backstage/filter-predicates@0.1.0 + +## @backstage/filter-predicates@0.1.0 + +### Minor Changes + +- 7feb83b: Introduced package, basically as the extracted predicate types from `@backstage/plugin-catalog-react/alpha` + +## @backstage/frontend-app-api@0.15.0 + +### Minor Changes + +- 55b2ef6: **BREAKING**: Updated the behavior of the new API override logic to reject the override and block app startup instead of just logging a deprecation warning. + +### Patch Changes + +- 7edb810: Implemented support for the `internal` extension input option. + +- 492503a: Updated error reporting and app tree resolution logic to attribute errors to the correct extension and allow app startup to proceed more optimistically: + + - If an attachment fails to provide the required input data, the error is now attributed to the attachment rather than the parent extension. + - Singleton extension inputs will now only forward attachment errors if the input is required. + - Array extension inputs will now filter out failed attachments instead of failing the entire app tree resolution. + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. + +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. + +- 9554c36: **DEPRECATED**: Deprecated support for multiple attachment points. + +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 09032d7: Internal update to simplify testing utility implementations. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/frontend-defaults@0.4.0 + - @backstage/core-app-api@1.19.5 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-defaults@0.4.0 + +### Minor Changes + +- 55b2ef6: **BREAKING**: The `API_FACTORY_CONFLICT` warning is now treated as an error and will prevent the app from starting. + +### Patch Changes + +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. +- c38b74d: Dependency update for tests. +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/frontend-app-api@0.15.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app@0.4.0 + +## @backstage/frontend-plugin-api@0.14.0 + +### Minor Changes + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. + +- bb9b471: Plugin IDs that do not match the standard format are deprecated (letters, digits, and dashes only, starting with a letter). Plugin IDs that do no match this format will be rejected in a future release. + +- ef6916e: Added `SubPageBlueprint` for creating sub-page tabs, `PluginHeaderActionBlueprint` and `PluginHeaderActionsApi` for plugin-scoped header actions, and `PageLayout` as a swappable component. The `PageBlueprint` now supports sub-pages with tabbed navigation, page title, icon, and header actions. Plugins can now specify a `title` and `icon` in `createFrontendPlugin`. + +- c38b74d: **BREAKING**: The following blueprints have been removed and are now only available from `@backstage/plugin-app-react`: + + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- 10ebed4: **BREAKING**: Removed type support for multiple attachment points in the `ExtensionDefinitionAttachTo` type. Extensions can no longer specify an array of attachment points in the `attachTo` property. + + The runtime still supports multiple attachment points for backward compatibility with existing compiled code, but new code will receive type errors if attempting to use this pattern. + + Extensions that previously used multiple attachment points should migrate to using a Utility API pattern instead. See the [Sharing Extensions Across Multiple Locations](https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions) guide for the recommended approach. + +### Patch Changes + +- 7edb810: Added a new `internal` option to `createExtensionInput` that marks the input as only allowing attachments from the same plugin. + +- 9554c36: **DEPRECATED**: Multiple attachment points for extensions have been deprecated. The functionality continues to work for backward compatibility, but will log a deprecation warning and be removed in a future release. + + Extensions using array attachment points should migrate to using Utility APIs instead. See the [Sharing Extensions Across Multiple Locations](https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions) guide for the recommended pattern. + +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-test-utils@0.5.0 + +### Minor Changes + +- 09a6aad: **BREAKING**: Removed the `TestApiRegistry` class, use `TestApiProvider` directly instead, storing reused APIs in a variable, e.g. `const apis = [...] as const`. +- d2ac2ec: Added `MockAlertApi` and `MockFeatureFlagsApi` implementations to the `mockApis` namespace. The mock implementations include useful testing methods like `clearAlerts()`, `waitForAlert()`, `getState()`, `setState()`, and `clearState()` for better test ergonomics. +- 09a6aad: **BREAKING**: The `mockApis` namespace is no longer a re-export from `@backstage/test-utils`. It's now a standalone namespace with mock implementations of most core APIs. Mock API instances can be passed directly to `TestApiProvider`, `renderInTestApp`, and `renderTestApp` without needing `[apiRef, impl]` tuples. As part of this change, the `.factory()` method on some mocks has been removed, since it's now redundant. + + ```tsx + // Before + import { mockApis } from '@backstage/frontend-test-utils'; + + renderInTestApp(, { + apis: [[identityApiRef, mockApis.identity()]], + }); + + // After - mock APIs can be passed directly + renderInTestApp(, { + apis: [mockApis.identity()], + }); + ``` + + This change also adds `createApiMock`, a public utility for creating mock API factories, intended for plugin authors to create their own `.mock()` variants. + +### Patch Changes + +- 22864b7: Added an `apis` option to `createExtensionTester`, `renderInTestApp`, and `renderTestApp` to override APIs when testing extensions. Use the `mockApis` helpers to create mock implementations: + + ```typescript + import { identityApiRef } from '@backstage/frontend-plugin-api'; + import { mockApis } from '@backstage/frontend-test-utils'; + + // Override APIs in createExtensionTester + const tester = createExtensionTester(myExtension, { + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + + // Override APIs in renderInTestApp + renderInTestApp(, { + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + + // Override APIs in renderTestApp + renderTestApp({ + extensions: [myExtension], + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + ``` + +- 15ed3f9: Added `snapshot()` method to `ExtensionTester`, which returns a tree-shaped representation of the resolved extension hierarchy. Convenient to use with `toMatchInlineSnapshot()`. + +- 013ec22: Added `mountedRoutes` option to `renderTestApp` for binding route refs to paths, matching the existing option in `renderInTestApp`: + + ```typescript + renderTestApp({ + extensions: [...], + mountedRoutes: { + '/my-path': myRouteRef, + }, + }); + ``` + +- d7dd5bd: Fixed Router deprecation warning and switched to using new `RouterBlueprint` from `@backstage/plugin-app-api`. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/frontend-app-api@0.15.0 + - @backstage/core-app-api@1.19.5 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-app@0.4.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/test-utils@1.7.15 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/integration@1.20.0 + +### Minor Changes + +- 6999f6d: The AzureUrl class in the @backstage/integration package is now able to process BOTH git branches and git tags. Initially this class only processed git branches and threw an error when non-branch Azure URLs were passed in. + +### Patch Changes + +- cc6206e: Added support for `{org}.visualstudio.com` domains used by Azure DevOps +- 7455dae: Use node prefix on native imports + +## @backstage/module-federation-common@0.1.0 + +### Minor Changes + +- ce12dec: Added new `@backstage/module-federation-common` package that provides shared types, default configurations, and runtime utilities for module federation. It includes `loadModuleFederationHostShared` for loading shared dependencies in parallel at runtime, `defaultHostSharedDependencies` and `defaultRemoteSharedDependencies` for consistent dependency configuration, and types such as `HostSharedDependencies`, `RemoteSharedDependencies`, and `RuntimeSharedDependenciesGlobal`. + +## @backstage/ui@0.12.0 + +### Minor Changes + +- 46a9adc: **BREAKING**: Alert no longer accepts a `surface` prop + + The Alert component's background is now driven entirely by its `status` prop. The `surface` prop has been removed. + + ```diff + - + + + ``` + + **Affected components:** Alert + +- b63c25b: **BREAKING**: Removed gray scale tokens and renamed background surface tokens to neutral tokens + + The `--bui-gray-1` through `--bui-gray-8` tokens have been removed. The `--bui-bg-surface-*` and `--bui-bg-neutral-on-surface-*` tokens have been replaced by a unified `--bui-bg-neutral-*` scale. + + **Migration:** + + Replace surface tokens directly: + + ```diff + - background: var(--bui-bg-surface-0); + + background: var(--bui-bg-neutral-0); + ``` + + Replace on-surface tokens shifted by +1: + + ```diff + - background: var(--bui-bg-neutral-on-surface-0); + + background: var(--bui-bg-neutral-1); + ``` + + Replace gray tokens 1-4 with neutral equivalents (`--bui-gray-5` through `--bui-gray-8` have no direct replacement): + + ```diff + - background: var(--bui-gray-1); + + background: var(--bui-bg-neutral-1); + ``` + +- 7898df0: **BREAKING**: Replaced `Surface` / `onSurface` system with new provider/consumer background system + + The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer `bg` architecture. + + **Types:** + + - `ContainerBg` — `'neutral-1'` \| `'neutral-2'` \| `'neutral-3'` \| `'danger'` \| `'warning'` \| `'success'` + - `ProviderBg` — `ContainerBg | 'neutral-auto'` + + Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. See "Neutral level capping" below for details on how levels are bounded. + + **Hooks:** + + - `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no `bg` is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. + - `useBgConsumer()` — for consumer components. Returns the parent container's `bg` unchanged. + + **Component roles:** + + - **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. **Transparent by default** — they do _not_ auto-increment; pass `bg="neutral-auto"` explicitly if you want automatic neutral stepping. + - **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit the parent container's `bg` unchanged. + - **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Card passes `bg="neutral-auto"` to its inner Box, so it auto-increments from the parent context. + + **Neutral level capping:** + + Provider components cap at `neutral-3`. There is no `neutral-4` prop value. The `neutral-4` level exists only in consumer component CSS — for example, a Button sitting on a `neutral-3` surface uses `neutral-4` tokens internally via `data-on-bg`. + + **Migration:** + + Rename the `surface` prop to `bg` on provider components and update values: + + ```diff + - + + + + - + + + + - + + + + - + + + ``` + + Remove `onSurface` from consumer components — they now always inherit from the parent container: + + ```diff + - + + + ``` + + **Affected components:** Button + +- 110fec0: **BREAKING**: Removed link and tint color tokens, added new status foreground tokens, and improved Link component styling + + The following color tokens have been removed: + + - `--bui-fg-link` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-fg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-bg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-border-tint` (and all related tokens) + + **New Status Tokens:** + + Added dedicated tokens for status colors that distinguish between usage on status backgrounds vs. standalone usage: + + - `--bui-fg-danger-on-bg` / `--bui-fg-danger` + - `--bui-fg-warning-on-bg` / `--bui-fg-warning` + - `--bui-fg-success-on-bg` / `--bui-fg-success` + - `--bui-fg-info-on-bg` / `--bui-fg-info` + + The `-on-bg` variants are designed for text on colored backgrounds, while the base variants are for standalone status indicators with improved visibility and contrast. + + **Migration:** + + For link colors, migrate to one of the following alternatives: + + ```diff + .custom-link { + - color: var(--bui-fg-link); + + color: var(--bui-fg-info); /* For informational links */ + + /* or */ + + color: var(--bui-fg-primary); /* For standard text links */ + } + ``` + + For tint colors (backgrounds, foregrounds, borders), migrate to appropriate status or neutral colors: + + ```diff + .info-section { + - background: var(--bui-bg-tint); + + background: var(--bui-bg-info); /* For informational sections */ + + /* or */ + + background: var(--bui-bg-neutral-1); /* For neutral emphasis */ + } + ``` + + If you're using status foreground colors on colored backgrounds, update to the new `-on-bg` tokens: + + ```diff + .error-badge { + - color: var(--bui-fg-danger); + + color: var(--bui-fg-danger-on-bg); + background: var(--bui-bg-danger); + } + ``` + + **Affected components:** Link + +### Patch Changes + +- 644e303: Added a new `FullPage` component that fills the remaining viewport height below the `PluginHeader`. + + ```tsx + + + {/* content fills remaining height */} + + ``` + + **Affected components:** FullPage + +- 44877e4: Fixed dark theme `--bui-fg-secondary` and `--bui-fg-disabled` tokens using black-based `oklch(0% ...)` instead of white-based `oklch(100% ...)`, making secondary and disabled text visible on dark backgrounds. + +- 350c948: Fixed Box component to forward HTML attributes to the underlying div element. + + **Affected components:** Box + +- 7455dae: Use node prefix on native imports + +- c8ae765: Fixed nested Accordion icon state issue where the inner accordion's arrow icon would incorrectly show as expanded when only the outer accordion was expanded. The CSS selector now uses a direct parent selector to ensure the icon only responds to its own accordion's expanded state. + + Affected components: Accordion + +- 4d1b7f4: Fixed CSS Module syntax to comply with Next.js 16 Turbopack validation by flattening nested dark theme selectors. + + **Affected components:** Popover, Tooltip + +- 2c219b9: Added `destructive` prop to Button for dangerous actions like delete or remove. Works with all variants (primary, secondary, tertiary). + + **Affected components:** Button + +- 5af9e14: Fixed `useDefinition` hook adding literal "undefined" class name when no className prop was passed. + +- 5c76d13: Allow `ref` as a prop on the `Tag` component + + Affected components: Tag + +- ab25658: Cleaned up `useDefinition` `ownProps` types to remove never-typed ghost properties from autocomplete. + +- 741a98d: Allow data to be passed directly to the `useTable` hook using the property `data` instead of `getData()` for mode `"complete"`. + + This simplifies usage as data changes, rather than having to perform a `useEffect` when data changes, and then reloading the data. It also happens immediately, so stale data won't remain until a rerender (with an internal async state change), so less flickering. + + Affected components: Table + +- a0fe1b2: Fixed changing columns after first render from crashing. It now renders the table with the new column layout as columns change. + + Affected components: Table + +- 508bd1a: Added new `Alert` component with support for status variants (info, success, warning, danger), icons, loading states, and custom actions. + + Updated status color tokens for improved contrast and consistency across light and dark themes: + + - Added new `--bui-bg-info` and `--bui-fg-info` tokens for info status + - Updated `--bui-bg-danger`, `--bui-fg-danger` tokens + - Updated `--bui-bg-warning`, `--bui-fg-warning` tokens + - Updated `--bui-bg-success`, `--bui-fg-success` tokens + + **Affected components**: Alert + +- da30862: Fixed client-side navigation for container components by wrapping the container (not individual items) in RouterProvider. Components now conditionally provide routing context only when children have internal links, removing the Router context requirement when not needed. This also removes the need to wrap these components in MemoryRouter during tests when they are not using the `href` prop. + + Additionally, when multiple tabs match the current URL via prefix matching, the tab with the most specific path (highest segment count) is now selected. For example, with URL `/catalog/users/john`, a tab with path `/catalog/users` is now selected over a tab with path `/catalog`. + + Affected components: Tabs, Tab, TagGroup, Tag, Menu, MenuItem, MenuAutocomplete + +- 092c453: Fixed an infinite render loop in Tabs when navigating to a URL that doesn't match any tab `href`. + +- becf851: export PasswordField component + +- becee36: Migrated Accordion components to use `useDefinition` instead of `useStyles`, and added automatic background adaptation based on parent container context. + +- 5320aa8: Fixed components to not require a Router context when rendering without internal links. + + Affected components: Link, ButtonLink, Row + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 8c39412: The Table component now wraps the react-aria-components `Table` with a `ResizableTableContainer` only if any column has a width property set. This means that column widths can adapt to the content otherwise (if no width is explicitly set). + + Affected components: Table + +- cb090b4: Bump react-aria-components to v1.14.0 + +- c429101: Fixed React 17 compatibility by using `useId` from `react-aria` instead of the built-in React hook which is only available in React 18+. + +- 74c5a76: Fixed Switch component disabled state styling to show `not-allowed` cursor and disabled text color. + + **Affected components:** Switch + +- 20131c5: Migrated to use the standard `backstage-cli package build` for CSS bundling instead of a custom build script. + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-app@0.4.0 + +### Minor Changes + +- ef6916e: Added `SubPageBlueprint` for creating sub-page tabs, `PluginHeaderActionBlueprint` and `PluginHeaderActionsApi` for plugin-scoped header actions, and `PageLayout` as a swappable component. The `PageBlueprint` now supports sub-pages with tabbed navigation, page title, icon, and header actions. Plugins can now specify a `title` and `icon` in `createFrontendPlugin`. +- 7edb810: **BREAKING**: Extensions created with the following blueprints must now be provided via an override or a module for the `app` plugin. Extensions from other plugins will now trigger a warning in the app and be ignored. + + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +### Patch Changes + +- a2133be: Added new `NavContentNavItem`, `NavContentNavItems`, and `navItems` prop to `NavContentComponentProps` for auto-discovering navigation items from page extensions. The new `navItems` collection supports `take(id)` and `rest()` methods for placing specific items in custom sidebar positions, as well as `withComponent(Component)` which returns a `NavContentNavItemsWithComponent` for rendering items directly as elements. The existing `items` prop is now deprecated in favor of `navItems`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-app-react@0.2.0 + +### Minor Changes + +- a2133be: Added new `NavContentNavItem`, `NavContentNavItems`, and `navItems` prop to `NavContentComponentProps` for auto-discovering navigation items from page extensions. The new `navItems` collection supports `take(id)` and `rest()` methods for placing specific items in custom sidebar positions, as well as `withComponent(Component)` which returns a `NavContentNavItemsWithComponent` for rendering items directly as elements. The existing `items` prop is now deprecated in favor of `navItems`. + +### Patch Changes + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- 409af72: Internal refactor to move implementation of blueprints from `@backstage/frontend-plugin-api` to this package. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-app-visualizer@0.2.0 + +### Minor Changes + +- ef6916e: Migrated to use `SubPageBlueprint` for tabbed navigation and added a copy-tree-as-JSON plugin header action using `PluginHeaderActionBlueprint`. The plugin now specifies a `title` and `icon`. + +### Patch Changes + +- cb090b4: Bump react-aria-components to v1.14.0 +- c38b74d: Internal updates for blueprint moves to `@backstage/plugin-app-react`. +- 4137a43: Updated CSS token references to use renamed `--bui-border-2` token. +- 4d50e1f: Improved rendering performance of the details page. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-auth-backend@0.27.0 + +### Minor Changes + +- 31de2c9: Added experimental support for Client ID Metadata Documents (CIMD). + + This allows Backstage to act as an OAuth 2.0 authorization server that supports the [IETF Client ID Metadata Document draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). External OAuth clients can use HTTPS URLs as their `client_id`, and Backstage will fetch metadata from those URLs to validate the client. + + **Configuration example:** + + ```yaml + auth: + experimentalClientIdMetadataDocuments: + enabled: true + # Optional: restrict which `client_id` URLs are allowed (defaults to ['*']) + allowedClientIdPatterns: + - 'https://example.com/*' + - 'https://*.trusted-domain.com/*' + # Optional: restrict which redirect URIs are allowed (defaults to ['*']) + allowedRedirectUriPatterns: + - 'http://localhost:*' + - 'https://*.example.com/*' + ``` + + Clients using CIMD must host a JSON metadata document at their `client_id` URL containing at minimum: + + ```json + { + "client_id": "https://example.com/.well-known/oauth-client/my-app", + "client_name": "My Application", + "redirect_uris": ["http://localhost:8080/callback"], + "token_endpoint_auth_method": "none" + } + ``` + +- d0786b9: Added experimental support for refresh tokens via the `auth.experimentalRefreshToken.enabled` configuration option. When enabled, clients can request the `offline_access` scope to receive refresh tokens that can be used to obtain new access tokens without re-authentication. + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.3.0 + +### Minor Changes + +- 36804fe: feat: Added organization option to authorization params of the strategy + +### Patch Changes + +- 867c905: Add support for organizational invites in auth0 strategy +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-github-provider@0.5.0 + +### Minor Changes + +- ff07934: Added the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by their GitHub user ID. + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.4.0 + +### Minor Changes + +- ff07934: Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-catalog@1.33.0 + +### Minor Changes + +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 05aac34: Migrated `DeleteEntityDialog` and `EntityOrphanWarning` components to Backstage UI. + + The `deleteEntity.description` translation key no longer includes "Click here to delete" text. A new `deleteEntity.actionButtonTitle` key was added for the action button. + +### Patch Changes + +- 220d6c3: Add missing translation entries for catalog UI text. + + This change adds translation keys and updates relevant UI components to use the correct localized labels and text in the catalog plugin. It ensures that catalog screens such as entity layout, tabs, search result items, table labels, and other UI elements correctly reference the i18n system for translation. + + No functional behavior is changed aside from the improved internationalization support. + +- 8d4c48b: Fixed vertical spacing between tags in the catalog table. + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. + +- e8258d0: The default entity content layout still supports rendering summary cards at runtime for backward compatibility, but logs a console warning when they are detected to help identify where migration is needed. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 75ac651: Migrated `EntityRelationWarning` and `EntityProcessingErrorsPanel` components from Material UI to Backstage UI. + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-catalog-backend@3.4.0 + +### Minor Changes + +- f1d29b4: Failures to connect catalog providers are now attributed to the module that provided the failing provider. This means that such failures will be reported as module startup failures rather than a failure to start the catalog plugin, and will therefore respect `onPluginModuleBootFailure` configuration instead. + +- 34cc520: Implemented handling of events from the newly introduced alpha + `catalogScmEventsServiceRef` service, in the builtin entity providers. This + allows entities to get refreshed, and locations updated or removed, as a + response to incoming events. In its first iteration, only the GitHub module + implements such event handling however. + + This is not yet enabled by default, but this fact may change in a future + release. To try it out, ensure that you have the latest catalog GitHub module + installed, and set the following in your app-config: + + ```yaml + catalog: + scmEvents: true + ``` + + Or if you want to pick and choose from the various features: + + ```yaml + catalog: + scmEvents: + # refresh (reprocess) upon events? + refresh: true + # automatically unregister locations based on events? (files deleted, repos archived, etc) + unregister: true + # automatically move locations based on events? (repo transferred, file renamed, etc) + move: true + ``` + +- b4e8249: Implemented the `POST /locations/by-query` endpoint which allows paginated, filtered location queries + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 08a5813: Fixed O(n²) performance bottleneck in `buildEntitySearch` `traverse()` by replacing `Array.some()` linear scan with a `Set` for O(1) duplicate path key detection. +- 1e669cc: Migrate audit events reference docs to . +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.0 + +### Minor Changes + +- 2f51676: allow entity discoverability via gitlab search API +- ff07934: Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 7e6b5e5: Fixed GitLab search API scope parameter from `'blob'` to `'blobs'`, resolving 400 errors in discovery provider. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-msgraph@0.9.0 + +### Minor Changes + +- 8694561: Log group/user count, tenant ID, execution time as separate fields + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-node@2.0.0 + +### Minor Changes + +- cfd8103: Promoted stable catalog extension points from alpha to main export. The following extension points are now exported from `@backstage/plugin-catalog-node` instead of `@backstage/plugin-catalog-node/alpha`: + + - `catalogLocationsExtensionPoint` and `CatalogLocationsExtensionPoint` + - `catalogProcessingExtensionPoint` and `CatalogProcessingExtensionPoint` + - `catalogAnalysisExtensionPoint` and `CatalogAnalysisExtensionPoint` + + The old alpha exports for these extension points are now deprecated with `@deprecated` markers pointing to the new stable exports. Please update your imports from `@backstage/plugin-catalog-node/alpha` to `@backstage/plugin-catalog-node`. + + Note: The `catalogModelExtensionPoint`, `catalogPermissionExtensionPoint`, and related types remain in alpha. + +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries + +- 34cc520: Introduced the `catalogScmEventsServiceRef`, along with `CatalogScmEventsService` and associated types. These allow communicating a unified set of events, that parts of the catalog can react to. + +### Patch Changes + +- 42abfb1: Updated `catalogServiceMock.mock` to use `createServiceMock` from `@backstage/backend-test-utils`, replacing the internal copy of `simpleMock`. Added `@backstage/backend-test-utils` as an optional peer dependency. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-test-utils@1.11.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-react@2.0.0 + +### Minor Changes + +- 0e9578d: Migrated `UnregisterEntityDialog` from Material UI to Backstage UI components. + +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) + +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries + +- 7feb83b: **BREAKING ALPHA**: All of the predicate types and functions have been moved to the `@backstage/filter-predicates` package. + + When moving into the more general package, they were renamed as follows: + + - `EntityPredicate` -> `FilterPredicate` + - `EntityPredicateExpression` -> `FilterPredicateExpression` + - `EntityPredicatePrimitive` -> `FilterPredicatePrimitive` + - `entityPredicateToFilterFunction` -> `filterPredicateToFilterFunction` + - `EntityPredicateValue` -> `FilterPredicateValue` + +- e8258d0: **BREAKING**: Removed the 'summary' entity card type from `EntityCardType`. Users should migrate to using 'content' or 'info' card types instead. + + TypeScript will now show errors if you try to use `type: 'summary'` when creating entity cards. + +- ac9bead: Added `createTestEntityPage` test utility for testing entity cards and content extensions in the new frontend system. This utility creates a test page extension that provides `EntityProvider` context and accepts entity extensions through input redirects: + + ```typescript + import { renderTestApp } from '@backstage/frontend-test-utils'; + import { createTestEntityPage } from '@backstage/plugin-catalog-react/testUtils'; + + renderTestApp({ + extensions: [createTestEntityPage({ entity: myEntity }), myEntityCard], + }); + ``` + +### Patch Changes + +- f523983: Fixes a bug where the `EntityListProvider` would not correctly hydrate query parameters if more than 20 were provided for the same key. +- 09a6aad: The `catalogApiMock` test utility now returns a `MockWithApiFactory`, allowing it to be passed directly to test utilities like `renderTestApp` and `TestApiProvider` without needing the `[catalogApiRef, catalogApiMock()]` tuple. +- 88dbd5e: fixed bug in `UserListPicker` by getting the `kindParamater` from the `filters` rather than from the `queryParameters` +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-test-utils@0.5.0 + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-events-backend-module-google-pubsub@0.2.0 + +### Minor Changes + +- 80905b3: Added an optional `filter` property to PubSub consumers/publishers + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-search@1.6.0 + +### Minor Changes + +- feef8d9: Added support for configuring the default search type in the search page via the `search.defaultType` option in `app-config.yaml`. This applies to both the legacy and new frontend systems. If not set, the default is empty, which means searching for "all" types. + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-react@1.10.3 + +## @backstage/plugin-search-backend-module-elasticsearch@1.8.0 + +### Minor Changes + +- 583bd3a: Added `elasticsearchAuthExtensionPoint` to enable dynamic authentication mechanisms such as bearer tokens with automatic rotation. + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 7021165: Fixed bulk indexing to refresh only the target index instead of all indexes, improving performance in multi-index deployments. +- Updated dependencies + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-techdocs@1.17.0 + +### Minor Changes + +- 27798df: Add two config values to the `page:techdocs/reader` extension that configure default layout, `withoutSearch` and `withoutHeader`. Default are unchanged to `false`. + + E.g. to disable the search and header on the Techdocs Reader Page: + + ```yaml + app: + extensions: + - page:techdocs/reader: + config: + withoutSearch: true + withoutHeader: true + ``` + +### Patch Changes + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 9e29545: Improve sidebars (nav/TOC) layout and scrolling +- 22dce2b: TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 0a88779: Added title prop to OffsetPaginatedDocsTable for proper display +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + +## @backstage/plugin-user-settings@0.9.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-user-settings-common@0.1.0 + - @backstage/plugin-signals-react@0.0.19 + +## @backstage/plugin-user-settings-backend@0.4.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + +### Patch Changes + +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-user-settings-common@0.1.0 + - @backstage/plugin-signals-node@0.1.28 + +## @backstage/plugin-user-settings-common@0.1.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + +## @backstage/app-defaults@1.7.5 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + +## @backstage/backend-defaults@0.15.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 44f5d04: Minor internal restructure of the postgres config loading code +- 4fc7bf0: Bump to tar v7 +- 5dd683f: `createRateLimitMiddleware` is now exported from `@backstage/backend-defaults/httpRouter` +- 8dd518a: Support `connection.type: azure` in database client to use Microsoft Entra authentication with Azure database for PostgreSQL +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-app-api@1.5.0 + - @backstage/integration@1.20.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config-loader@1.10.8 + - @backstage/cli-node@0.2.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/backend-dev-utils@0.1.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + +## @backstage/backend-dynamic-feature-service@0.7.9 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. +- 9b4c414: Updated README for backend-dynamic-feature-service +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/config-loader@1.10.8 + - @backstage/plugin-events-backend@0.5.11 + - @backstage/plugin-search-common@1.2.22 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-app-node@0.1.42 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/backend-openapi-utils@0.6.6 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/cli@0.35.4 + +### Patch Changes + +- cfd8103: Updated catalog provider module template to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of alpha exports. + +- 20131c5: Added support for CSS exports in package builds. When a package declares a CSS file in its `exports` field (e.g., `"./styles.css": "./src/styles.css"`), the CLI will automatically bundle it during `backstage-cli package build`, resolving any `@import` statements. The export path is rewritten from `src/` to `dist/` at publish time. + + Fixed `backstage-cli repo fix` to not add `typesVersions` entries for non-script exports like CSS files. + +- 7455dae: Use node prefix on native imports + +- 6ce4a13: Removed `/alpha` from `scaffolderActionsExtensionPoint` import + +- fdbd404: Removed the `EXPERIMENTAL_MODULE_FEDERATION` environment variable flag, making module federation host support always available during `package start`. The host shared dependencies are now managed through `@backstage/module-federation-common` and injected as a versioned runtime script at build time. + +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. + +- 4fc7bf0: Bump to tar v7 + +- 5e3ef57: Added support for the new `peerModules` metadata field in `package.json`. This field allows plugin packages to declare modules that should be installed alongside them for cross-plugin integrations. The field is validated by `backstage-cli repo fix --publish`. + +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. + +- 73351c2: Updated dependency `webpack` to `~5.104.0`. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/config-loader@1.10.8 + - @backstage/eslint-plugin@0.2.1 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + - @backstage/module-federation-common@0.1.0 + +## @backstage/cli-common@0.1.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + +## @backstage/cli-node@0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added support for the new `peerModules` metadata field in `package.json`. This field allows plugin packages to declare modules that should be installed alongside them for cross-plugin integrations. The field is validated by `backstage-cli repo fix --publish`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/cli-common@0.1.18 + +## @backstage/codemods@0.1.54 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/cli-common@0.1.18 + +## @backstage/config-loader@1.10.8 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/cli-common@0.1.18 + +## @backstage/core-app-api@1.19.5 + +### Patch Changes + +- 5a71e7a: Fixed memory leak caused by duplicate `AppThemeSelector` instances and missing cleanup in `AppThemeSelector` and `AppLanguageSelector`. Added `dispose()` method to both selectors for proper resource cleanup. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-compat-api@0.5.8 + +### Patch Changes + +- c38b74d: Internal updates for blueprint moves to `@backstage/plugin-app-react`. +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-components@0.18.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- cebfea7: Removed link styles from LinkButton to avoid styling inconsistencies related to import order. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-plugin-api@1.12.3 + +### Patch Changes + +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/version-bridge@1.0.12 + +## @backstage/create-app@0.7.9 + +### Patch Changes + +- 40f2720: Updated to include the missing core plugins in the template used with the `--next` flag. Also updated `react-router*` versions and added Jest 30-related dependencies. Finally, moved the order of `@playwright/test` so it won't trigger a file change during the creation process. +- 1ea737c: Bumped create-app version. +- 7c41134: Bumped create-app version. +- 65ba820: Updated the app template sidebar to use the new `NavContentBlueprint` API for page-based navigation. +- 7455dae: Use node prefix on native imports +- c38b74d: Switched `next-app` template to use blueprint from `@backstage/plugin-app-react`. +- Updated dependencies + - @backstage/cli-common@0.1.18 + +## @backstage/dev-utils@1.1.20 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/app-defaults@1.7.5 + +## @backstage/e2e-test-utils@0.1.2 + +### Patch Changes + +- b96c20e: Added optional `channel` option to `generateProjects()` to allow customizing the Playwright browser channel for testing against different browsers variants. When not provided, the function defaults to 'chrome' to maintain backward compatibility. + + Example usage: + + ```ts + import { generateProjects } from '@backstage/e2e-test-utils'; + + export default defineConfig({ + projects: generateProjects({ channel: 'msedge' }), + }); + ``` + +- 7455dae: Use node prefix on native imports + +## @backstage/eslint-plugin@0.2.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + +## @backstage/frontend-dynamic-feature-loader@0.1.9 + +### Patch Changes + +- fdbd404: Updated module federation integration to use `@module-federation/enhanced/runtime` `createInstance` API and the new `loadModuleFederationHostShared` from `@backstage/module-federation-common` for loading shared dependencies. Also added support for passing a pre-created `ModuleFederation` instance via the `moduleFederation.instance` option. +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/module-federation-common@0.1.0 + +## @backstage/integration-aws-node@0.1.20 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + +## @backstage/integration-react@1.2.15 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/repo-tools@0.16.4 + +### Patch Changes + +- cd75ed0: Add newline to OpenAPI license template files. +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Bump to tar v7 +- 6523040: Support Prettier v3 for api-reports +- be7ebad: Updated package-docs exclude list to reflect renamed example app packages. +- df59ee6: The `type-deps` command now follows relative imports and re-exports into declaration chunk files, and detects ambient global types such as the `jest` namespace. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + +## @techdocs/cli@1.10.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 27798df: Migrate the Techdocs CLI embedded app to the New Frontend System (NFS) +- 508d127: Updated dependency `find-process` to `^2.0.0`. +- Updated dependencies + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/cli-common@0.1.18 + +## @backstage/test-utils@1.7.15 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 68eb322: Added `@types/jest` as an optional peer dependency, since jest types are exposed in the public API surface. +- Updated dependencies + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/theme@0.7.2 + +### Patch Changes + +- 1c52dcc: add square shape +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +## @backstage/version-bridge@1.0.12 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +## @backstage/plugin-api-docs@0.13.4 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 7455dae: Use node prefix on native imports +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 629c3ec: Add `tableOptions` and `title` to Components cards of APIs +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-catalog@1.33.0 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-api-docs-module-protoc-gen-doc@0.1.11 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +## @backstage/plugin-app-backend@0.5.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-app-node@0.1.42 + +## @backstage/plugin-app-node@0.1.42 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + +## @backstage/plugin-auth@0.1.5 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.12 + +### Patch Changes + +- 08aea95: Added a validation check that rejects `audience` configuration values that are not absolute URLs (i.e. missing `https://` or `http://` prefix). +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + +## @backstage/plugin-auth-node@0.6.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + +## @backstage/plugin-auth-react@0.1.24 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-bitbucket-cloud-common@0.3.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.20 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-azure@0.3.14 + +### Patch Changes + +- cc6206e: Added support for `{org}.visualstudio.com` domains used by Azure DevOps +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 6c8a464: Added missing `branch` field to the `azureDevOps` provider config schema. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.11 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.8 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.8 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.16 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.11 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.9 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-github@0.12.2 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 34cc520: Implemented translation of webhook events into `catalogScmEventsServiceRef` events. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.2 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.18 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.8.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.9 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.2 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-logs@0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.17 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + +## @backstage/plugin-catalog-common@1.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-graph@0.5.7 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 8dd27c4: Fix large icon rendering in catalog graph nodes +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-catalog-import@0.13.10 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.26 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-devtools-react@0.1.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-config-schema@0.1.77 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-devtools@0.1.36 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- f2612c2: Fixes an issue where a user lacking permission to schedule tasks can now easily see the issue through a custom icon + tooltip. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-devtools-react@0.1.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-devtools-common@0.1.22 + +## @backstage/plugin-devtools-backend@0.5.14 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/cli-common@0.1.18 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-devtools-common@0.1.22 + +## @backstage/plugin-devtools-common@0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-devtools-react@0.1.1 + +### Patch Changes + +- 9fbb270: Updated dependency `@testing-library/react` to `^16.0.0`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-events-backend@0.5.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-azure@0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-gerrit@0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-github@0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-gitlab@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-module-kafka@0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-backend-test-utils@0.1.52 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-events-node@0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/plugin-gateway-backend@1.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/plugin-home@0.9.2 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 90956a6: Support new frontend system in the homepage plugin +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/core-compat-api@0.5.8 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + +## @backstage/plugin-home-react@0.1.35 + +### Patch Changes + +- 90956a6: Support new frontend system in the homepage plugin +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-kubernetes@0.12.16 + +### Patch Changes + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-react@0.5.16 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + +## @backstage/plugin-kubernetes-backend@0.21.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-node@0.4.1 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + +## @backstage/plugin-kubernetes-cluster@0.0.34 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-react@0.5.16 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + +## @backstage/plugin-kubernetes-common@0.9.10 + +### Patch Changes + +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-kubernetes-node@0.4.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-kubernetes-react@0.5.16 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- d56542c: Updated dependency `@xterm/addon-attach` to `^0.12.0`. + Updated dependency `@xterm/addon-fit` to `^0.11.0`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-mcp-actions-backend@0.1.9 + +### Patch Changes + +- 31de2c9: Added OAuth Protected Resource Metadata endpoint (`/.well-known/oauth-protected-resource`) per RFC 9728. This allows MCP clients to discover the authorization server for the resource. + + Also enabled OAuth well-known endpoints when CIMD (Client ID Metadata Documents) is configured, not just when DCR is enabled. + +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + +## @backstage/plugin-mui-to-bui@0.2.4 + +### Patch Changes + +- 4137a43: Updated CSS token references to use renamed `--bui-bg-app` and `--bui-border-2` tokens. + +- a88c437: Updated MUI to BUI theme converter to align with latest token changes + + **Changes:** + + - Removed generation of deprecated tokens: `--bui-fg-link`, `--bui-fg-link-hover`, `--bui-fg-tint`, `--bui-fg-tint-disabled`, `--bui-bg-tint` and all its variants + - Added generation of new `info` status tokens: `--bui-fg-info`, `--bui-fg-info-on-bg`, `--bui-bg-info`, `--bui-border-info` + - Updated status color mapping to generate both standalone and `-on-bg` variants for danger, warning, success, and info + - Status colors now use `.main` for standalone variants and `.dark` for `-on-bg` variants, providing better visual hierarchy + + The converter now generates tokens that match the updated BUI design system structure, with clear distinction between status colors for standalone use vs. use on colored backgrounds. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-notifications@0.5.14 + +### Patch Changes + +- 8005286: Added `renderItem` prop to `NotificationsSidebarItem` component, allowing custom UI rendering while retaining all built-in notification logic (unread count, snackbar, signals, web notifications). +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-signals-react@0.0.19 + +## @backstage/plugin-notifications-backend@0.6.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- e9eb400: Allow configuring included topics for email notifications. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + - @backstage/plugin-signals-node@0.1.28 + +## @backstage/plugin-notifications-backend-module-email@0.3.18 + +### Patch Changes + +- e9eb400: Allow configuring included topics for email notifications. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + +## @backstage/plugin-notifications-backend-module-slack@0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + +## @backstage/plugin-notifications-common@0.2.1 + +### Patch Changes + +- e9eb400: Allow configuring included topics for email notifications. + +## @backstage/plugin-notifications-node@0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.28 + +## @backstage/plugin-org@0.6.49 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 1dee6de: Add search functionality in MembersListCard +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-org-react@0.1.47 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-permission-backend@0.7.9 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + +## @backstage/plugin-permission-common@0.9.6 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features + +## @backstage/plugin-permission-node@0.10.10 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-permission-react@0.4.40 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-proxy-backend@0.6.10 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-proxy-node@0.1.12 + +## @backstage/plugin-proxy-node@0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @backstage/plugin-scaffolder@1.35.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + +- 4e581a6: Updated the browser tab title on the template wizard page to display the specific template title instead of the generic "Create a new component" text. + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. + +- 2eeca03: Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the following alpha exports were removed: + + - `formFieldsApiRef` + - `ScaffolderFormFieldsApi` + +- b9d90a7: Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-scaffolder-backend@3.1.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Removed unused dependency +- 0ce78b0: Support `if` conditions inside `each` loops for scaffolder steps +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- 1e669cc: Migrate audit events reference docs to . +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.3 + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.17 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.19 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.18 + - @backstage/plugin-scaffolder-backend-module-github@0.9.6 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.18 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.18 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.19 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.3 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 14741e2: Fully enable API token functionality for Bitbucket-Cloud. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.20 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.6 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 82ca951: cleaned up repo creation to make the unique portions explicit +- 672b972: Updated dependency `libsodium-wrappers` to `^0.8.0`. + Updated dependency `@types/libsodium-wrappers` to `^0.8.0`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.3 + +### Patch Changes + +- 6b5e7d9: Allow setting optional description on group creation +- 7455dae: Use node prefix on native imports +- f0f9403: Changed `gitlab:group:ensureExists` action to use `Groups.show` API instead of `Groups.search` for checking if a group path exists. This is more efficient as it directly retrieves the group by path rather than searching and filtering results. +- 32c51c0: Added new `gitlab:user:info` scaffolder action that retrieves information about a GitLab user. The action can fetch either the current authenticated user or a specific user by ID. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.19 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-scaffolder-node-test-utils@0.3.8 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-common@1.7.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-scaffolder-node@0.12.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Bump to tar v7 +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.8 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/backend-test-utils@1.11.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + +## @backstage/plugin-scaffolder-react@1.19.7 + +### Patch Changes + +- 2eeca03: Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the following alpha exports were removed: + + - `formFieldsApi` + - `formFieldsApiRef` + - `ScaffolderFormFieldsApi` + +- b9d90a7: Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- 69d880e: Bump to latest zod to ensure it has the latest features + +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-scaffolder-common@1.7.6 + +## @backstage/plugin-search-backend@2.0.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + +## @backstage/plugin-search-backend-module-catalog@0.3.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-search-backend-module-explore@0.3.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- df27350: Updated dependency `@backstage-community/plugin-explore-common` to `^0.12.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-pg@0.5.52 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 2ee354a: Return `numberOfResults` count with search query responses +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-techdocs@0.4.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-search-backend-node@1.4.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-search-common@1.2.22 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-search-react@1.10.3 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-signals@0.0.28 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-signals-react@0.0.19 + +## @backstage/plugin-signals-backend@0.3.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + - @backstage/plugin-signals-node@0.1.28 + +## @backstage/plugin-signals-node@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-events-node@0.4.19 + +## @backstage/plugin-signals-react@0.0.19 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-search-react@1.10.3 + - @backstage/test-utils@1.7.15 + +## @backstage/plugin-techdocs-backend@2.1.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/catalog-client@1.13.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.33 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + +## @backstage/plugin-techdocs-node@1.14.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 3c455d4: Some security fixes +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-techdocs-react@1.3.8 + +### Patch Changes + +- 22dce2b: TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the `techDocsAddonDataRef` alpha export was removed. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + +## example-app@0.0.32 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-defaults@0.4.0 + - @backstage/frontend-app-api@0.15.0 + - @backstage/plugin-app-visualizer@0.2.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-api-docs@0.13.4 + - @backstage/plugin-catalog-graph@0.5.7 + - @backstage/plugin-org@0.6.49 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-scaffolder@1.35.3 + - @backstage/plugin-notifications@0.5.14 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/plugin-kubernetes@0.12.16 + - @backstage/core-compat-api@0.5.8 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-app@0.4.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.26 + - @backstage/plugin-devtools@0.1.36 + - @backstage/plugin-home@0.9.2 + - @backstage/plugin-search@1.6.0 + - @backstage/plugin-user-settings@0.9.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.33 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-kubernetes-cluster@0.0.34 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-import@0.13.10 + - @backstage/app-defaults@1.7.5 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + - @backstage/plugin-signals@0.0.28 + - @backstage/plugin-auth@0.1.5 + - @backstage/plugin-catalog-common@1.1.8 + +## app-example-plugin@0.0.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + +## example-app-legacy@0.2.118 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-app-api@0.15.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-api-docs@0.13.4 + - @backstage/plugin-catalog-graph@0.5.7 + - @backstage/plugin-org@0.6.49 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-scaffolder@1.35.3 + - @backstage/plugin-notifications@0.5.14 + - @backstage/plugin-catalog@1.33.0 + - @backstage/plugin-mui-to-bui@0.2.4 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/plugin-kubernetes@0.12.16 + - @backstage/theme@0.7.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.26 + - @backstage/plugin-devtools@0.1.36 + - @backstage/plugin-home@0.9.2 + - @backstage/plugin-search@1.6.0 + - @backstage/plugin-user-settings@0.9.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.33 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-kubernetes-cluster@0.0.34 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-import@0.13.10 + - @backstage/app-defaults@1.7.5 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + - @backstage/plugin-signals@0.0.28 + - @backstage/plugin-catalog-common@1.1.8 + +## example-backend@0.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.11 + - @backstage/plugin-catalog-backend-module-openapi@0.2.19 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.17 + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.6 + - @backstage/plugin-search-backend-module-techdocs@0.4.11 + - @backstage/plugin-search-backend-module-catalog@0.3.12 + - @backstage/plugin-search-backend-module-explore@0.3.11 + - @backstage/plugin-notifications-backend@0.6.2 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-kubernetes-backend@0.21.1 + - @backstage/plugin-permission-backend@0.7.9 + - @backstage/plugin-scaffolder-backend@3.1.3 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-devtools-backend@0.5.14 + - @backstage/plugin-techdocs-backend@2.1.5 + - @backstage/plugin-signals-backend@0.3.12 + - @backstage/plugin-events-backend@0.5.11 + - @backstage/plugin-search-backend@2.0.12 + - @backstage/plugin-proxy-backend@0.6.10 + - @backstage/plugin-app-backend@0.5.11 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-mcp-actions-backend@0.1.9 + - @backstage/plugin-auth-backend-module-github-provider@0.5.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.16 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.4 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.8 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.16 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.19 + +## e2e-test@0.2.37 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.9 + - @backstage/cli-common@0.1.18 + +## @internal/frontend@0.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/version-bridge@1.0.12 + +## @internal/scaffolder@0.0.18 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-scaffolder-react@1.19.7 + +## techdocs-cli-embedded-app@0.2.117 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-defaults@0.4.0 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/test-utils@1.7.15 + +## yarn-plugin-backstage@0.0.9 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.18 + +## @internal/plugin-todo-list@1.0.48 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + +## @internal/plugin-todo-list-backend@1.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + +## @internal/plugin-todo-list-common@1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 diff --git a/docs/releases/v1.48.0.md b/docs/releases/v1.48.0.md new file mode 100644 index 0000000000..53b45c2f5f --- /dev/null +++ b/docs/releases/v1.48.0.md @@ -0,0 +1,130 @@ +--- +id: v1.48.0 +title: v1.48.0 +description: Backstage Release v1.48.0 +--- + +These are the release notes for the v1.48.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### **BREAKING ALPHA**: Catalog extension points graduated + +If you are providing custom processors and entity providers into the catalog, you will now note that several (but not quite all!) of those extension points have graduated out of alpha and into the regular stable exports. + +Thus, if you are importing for example `catalogProcessingExtensionPoint` from `@backstage/plugin-catalog-node/alpha`, you now want to remove that `/alpha` suffix. + +### **BREAKING**: API restrictions in New Frontend System + +In the 1.47 release a new behavior was introduced to the New Frontend System that limits the ability for plugins and modules to provide APIs to plugins other than themselves. For example, the `scaffolder` plugin could no longer install a custom `CatalogApi` implementation. This also applies to modules, where you now need to use a module explicitly targeting the `'app'` plugin to for example override the `ErrorApi`. + +In 1.47 this new behavior simply triggered a warning, but in this release the API overrides are instead rejected with an error. + +### Experimental Client ID Metadata Documents + +With the [latest MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) published in November, it outlined a new authorization method that’s going to take over from Dynamic Client Registration called [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents). + +This can be enabled in the `auth-backend` plugin by using the `auth.experimentalClientIdMetadataDocuments.enabled` flag in config. + +Be sure to head over to the [changelog](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/CHANGELOG.md) for additional configuration and security considerations. + +### Experimental Refresh Token Support + +Clients can now request the `offline_access` scope to receive refresh tokens when `auth.experimentalRefreshToken.enabled` is set, allowing new access tokens without re-authentication. This can be highly useful when using `experimentalDynamicClientRegistration` so that the tokens don’t expire every 1h by default, which then triggers another auth session approval request. + +This also means that `auth.experimentalDynamicClientRegistration.tokenExpiration` has been removed in favor of using this method as a **BREAKING EXPERIMENTAL** change. + +### New Frontend System: Testing utilities + +The testing utilities for the new frontend system have gotten an overhaul in this release, with all new features documented in the [plugin testing section](https://backstage.io/docs/frontend-system/building-plugins/testing). Some highlights include that `mountedRoutes` and `apis` options are available for all harnesses where applicable. The selection of `mockApis` has been increased to support more of the core APIs and have both Jest and minimal implementation mocks. The `mockApis` now also produces values that can be passed directly to the various testing `apis` options, without the need to wrap `[ref, impl]` tuples. + +### Utilities for creating frontend and backend mocks + +Both `@backstage/frontend-test-utils` and `@backstage/backend-test-utils` now export utilities for creating service and API mocks, `createServiceMock` and `createApiMock`. These utilities create the same kind of mock utilities as the ones provided by `mockServices` and `mockApis`, and are encouraged to be exported via the `/testUtils` sub-path export from packages, similar to how this pattern is used in `@backstage/plugin-catalog-react`. + +### New Frontend System: Plugin titles & icons + +Plugins now have optional titles and icons in the new frontend system. This helps implement app-wide features and navigation patterns that enumerate plugins or in other ways traverse the extension tree in the app. + +As part of this change we are also updating the icon type in the new frontend system. The existing `IconComponent` type is being deprecated, and is being replaced by the element-based `IconElement`. + +### New Frontend System: Page and nav structure + +This release starts incorporating the new navigation system from `@backstage/ui` into the New Frontend System. This comes in the form of an updated `PageBlueprint` that now supports sub-page navigation using a new `SubPageBlueprint`. This also introduces the new plugin-wide `PluginHeader`, although in this release it is limited to only being shown on plugin pages that use the new sub-page navigation. + +This change also updates the API provided by `NavContentBlueprint`. The `items` prop is now deprecated in favor of a new `navItems` prop, which is based on enumerating pages in the app rather than explicit nav items. The `navItems` prop also provides an API that makes it simpler to construct a nav bar that has explicit positioning of a few items, while leaving the rest to be rendered in a group. + +### New Frontend System: Home plugin support + +Added support for the New Frontend system to the Home plugin. + +Contributed by [@kunickiaj](https://github.com/kunickiaj) in [#31225](https://github.com/backstage/backstage/pull/31225). + +### New Frontend System: Entity summary card type removed + +The `summary` type has been removed from `EntityCardBlueprint`. This card type saw little use and feedback mostly pointed to it being confusing. Existing usage is encouraged to move to a `content` type card with a list of smaller cards instead. + +### New Frontend System: Removed multiple attachment points + +The ability for extensions to have multiple attachment points was introduced a couple of releases back in order to support use-cases where extensibility was needed across multiple extensions in a plugin. For example TechDocs addons needed to be available both at the top-level reader page as well as the reader installed as entity content. + +After observing this pattern in the wild, we concluded that it introduced more complexity than what it was worth, and support for multiple extension points has been removed. + +The alternative pattern, which is now used throughout the core feature plugins, is to use a Utility API to collect and share extensions. This pattern is now [documented in the architecture section](https://backstage.io/docs/next/frontend-system/architecture/extensions#sharing-extensions-across-multiple-locations) for the new frontend system. + +### New `@backstage/filter-predicates` Package + +Extracted predicate types from `@backstage/plugin-catalog-react/alpha` into a standalone package for general reuse. + +### New Backstage UI Components + Fixes + +New `Alert` component with support for status variants, icons, loading states, and custom actions. + +New `FullPage` component that fills the remaining viewport height below the `PluginHeader`. + +Added a `destructive` prop to the Button component for dangerous actions like delete or remove, working with all button variants. + +The `useTable` hook now accepts a direct `data` prop for mode `"complete"` instead of requiring `getData()`, reducing flickering when data changes. + +Contributed by [@grantila](https://github.com/grantila) in [#32685](https://github.com/backstage/backstage/pull/32685). + +### Experimental Catalog generic SCM event handling + +There’s a new experimental `catalogScmEventsServiceRef` in town. It’s a communication hub where event providers for example based on webhooks can feed distilled source code provider events into the catalog, where it can react in a generic way to them. This can enable instant refreshes of entities after you press Merge, automatic unregistration of locations when you decommission a repository, and much more. + +See [this issue](https://github.com/backstage/backstage/issues/32833) for more information on how you can contribute to fleshing out this capability! + +### Module Federation enabled by default + +Apps built with `@backstage/cli` now have module federation enabled by default without additional overhead. This is achieved by relying on the runtime APIs rather than introducing it at build-time. See the new [module federation section](https://backstage.io/docs/frontend-system/building-apps/module-federation) for more details and instructions for how to use this in your own app. Note that as part of this change the `EXPERIMENTAL_MODULE_FEDERATION` flag has been removed. + +Contributed by [@davidfestal](https://github.com/davidfestal) in [#30967](https://github.com/backstage/backstage/pull/30967). + +### Azure Database Microsoft Entra Authentication + +Added `connection.type: azure` in the database client config for Microsoft Entra authentication with Azure Database for PostgreSQL. + +Contributed by [@arjan07](https://github.com/arjan07) in [#31855](https://github.com/backstage/backstage/pull/31855). + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.48.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://spoti.fi/backstagenewsletter) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.49.0-next.0-changelog.md b/docs/releases/v1.49.0-next.0-changelog.md new file mode 100644 index 0000000000..fcfc092d5e --- /dev/null +++ b/docs/releases/v1.49.0-next.0-changelog.md @@ -0,0 +1,2362 @@ +# Release v1.49.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.49.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.49.0-next.0) + +## @backstage/cli-common@0.2.0-next.0 + +### Minor Changes + +- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths. + + To migrate existing `findPaths` usage: + + ```ts + // Before + import { findPaths } from '@backstage/cli-common'; + const paths = findPaths(__dirname); + + // After — for target project paths (cwd-based): + import { targetPaths } from '@backstage/cli-common'; + // paths.targetDir → targetPaths.dir + // paths.targetRoot → targetPaths.rootDir + // paths.resolveTarget('src') → targetPaths.resolve('src') + // paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock') + + // After — for package-relative paths: + import { findOwnPaths } from '@backstage/cli-common'; + const own = findOwnPaths(__dirname); + // paths.ownDir → own.dir + // paths.ownRoot → own.rootDir + // paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js') + // paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json') + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.7 + +## @backstage/integration@1.21.0-next.0 + +### Minor Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-catalog-backend@3.5.0-next.0 + +### Minor Changes + +- bf71677: Added opentelemetry metrics for SCM events: + + - `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`. + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- fbf382f: Minor internal optimisation +- 1ee5b28: Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes. +- 3181973: Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state` +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-catalog-node@2.1.0-next.0 + +### Minor Changes + +- bf71677: Added the ability for SCM events subscribers to mark the fact that they have taken actions based on events, which produces output metrics: + + - `catalog.events.scm.actions` with attribute `action`: Counter for the number of actions actually taken by catalog internals or other subscribers, based on SCM events. The `action` is currently either `create`, `delete`, `refresh`, or `move`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-notifications-backend-module-slack@0.4.0-next.0 + +### Minor Changes + +- 749ba60: Add an extension for custom Slack message layouts + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## @backstage/app-defaults@1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/backend-app-api@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/backend-defaults@0.15.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding + an optional `default` type discriminator to PostgreSQL connection configuration, + allowing `config:check` to properly validate `default` connection configurations. +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-dynamic-feature-service@0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/backend-openapi-utils@0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/backend-plugin-api@1.7.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-test-utils@1.11.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds a new metrics service mock to be leveraged in tests +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/catalog-client@1.13.1-next.0 + +### Patch Changes + +- d2494d6: Minor update to catalog client docs +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## @backstage/cli@0.35.5-next.0 + +### Patch Changes + +- 246877a: Updated dependency `bfj` to `^9.0.2`. + +- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`. + +- fd50cb3: Added `translations export` and `translations import` commands for managing translation files. + + The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app. + + Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping. + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. + +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. + +- 092b41f: Updated dependency `webpack` to `~5.105.0`. + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/module-federation-common@0.1.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/cli-node@0.2.19-next.0 + +### Patch Changes + +- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/codemods@0.1.55-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## @backstage/config-loader@1.10.9-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-app-api@1.19.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-compat-api@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## @backstage/core-components@0.18.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-plugin-api@1.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/create-app@0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## @backstage/dev-utils@1.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + +## @backstage/eslint-plugin@0.2.2-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + +## @backstage/frontend-app-api@0.15.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-defaults@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-app-api@0.15.1-next.0 + +## @backstage/frontend-dynamic-feature-loader@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/module-federation-common@0.1.0 + +## @backstage/frontend-plugin-api@0.14.2-next.0 + +### Patch Changes + +- 9c81af9: Made the `pluginId` property optional in the `FrontendFeature` type, allowing plugins published against older versions of the framework to be used without type errors. +- Updated dependencies + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-test-utils@0.5.1-next.0 + +### Patch Changes + +- 909c742: Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/integration-react@1.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/repo-tools@0.16.6-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 2a51546: Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 18a946c: Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig` +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @techdocs/cli@1.10.6-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/test-utils@1.7.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/ui@0.12.1-next.0 + +### Patch Changes + +- a1f4bee: Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components. + +- 8909359: Fixed focus-visible outline styles for Menu and Select components. + + **Affected components:** Menu, Select + +- 0f462f8: Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type. + +- 8909359: Added proper cursor styles for RadioGroup items. + + **Affected components:** RadioGroup + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-api-docs@0.13.5-next.0 + +### Patch Changes + +- 9c9d425: Fixed invisible text in parameter input fields when using dark mode in OpenAPI definition pages +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app@0.4.1-next.0 + +### Patch Changes + +- 909c742: Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app-backend@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-app-node@0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-app-react@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-app-visualizer@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-auth@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + +## @backstage/plugin-auth-backend@0.27.1-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 619be54: Update migrations to be reversible +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-node@0.6.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-react@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + +## @backstage/plugin-catalog@1.33.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-azure@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-github@0.12.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.3-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.0 + +### Patch Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-logs@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-graph@0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-import@0.13.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-react@2.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/frontend-test-utils@0.5.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-devtools-react@0.1.2-next.0 + +## @backstage/plugin-config-schema@0.1.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-devtools@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-devtools-backend@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-devtools-react@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-events-backend@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-azure@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-github@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-kafka@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-node@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-gateway-backend@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-home@0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## @backstage/plugin-home-react@0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-kubernetes@0.12.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-backend@0.21.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-node@0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-kubernetes-react@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + +### Patch Changes + +- dc81af1: Adds two new metrics to track MCP server operations and sessions. + + - `mcp.server.operation.duration`: The duration taken to process an individual MCP operation + - `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-mui-to-bui@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + +## @backstage/plugin-notifications@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## @backstage/plugin-notifications-backend@0.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-notifications-backend-module-email@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## @backstage/plugin-notifications-node@0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-org@0.6.50-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-org-react@0.1.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-permission-backend@0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-permission-node@0.10.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-permission-react@0.4.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-proxy-backend@0.6.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-proxy-node@0.1.13-next.0 + +## @backstage/plugin-proxy-node@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-scaffolder@1.35.5-next.0 + +### Patch Changes + +- bd5b842: Added a new `ui:autoSelect` option to the EntityPicker field that controls whether an entity is automatically selected when the field loses focus. When set to `false`, the field will remain empty if the user closes it without explicitly selecting an entity, preventing unintentional selections. Defaults to `true` for backward compatibility. +- ee87720: Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-scaffolder-backend@3.1.4-next.0 + +### Patch Changes + +- 4e39e63: Removed unused dependencies +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.4-next.0 + +### Patch Changes + +- 5730c8e: Added `maskedAndHidden` option to `gitlab:projectVariable:create` and `publish:gitlab` action to support creating GitLab project variables that are both masked and hidden. Updated gitbeaker to version 43.8.0 for proper type support. +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.0 + +## @backstage/plugin-scaffolder-common@1.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-scaffolder-node@0.12.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-react@1.19.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-search@1.6.2-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend@2.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-pg@0.5.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/plugin-search-backend-node@1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-react@1.10.5-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-signals@0.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## @backstage/plugin-signals-backend@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-signals-node@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-signals-react@0.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs@1.17.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-backend@2.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-node@1.14.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-techdocs-react@1.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings@0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## @backstage/plugin-user-settings-backend@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## example-app@0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/plugin-app-visualizer@0.2.1-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## app-example-plugin@0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + +## example-app-legacy@0.2.119-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.5-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## example-backend@0.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-scaffolder-backend@3.1.4-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-kubernetes-backend@0.21.2-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + - @backstage/plugin-search-backend@2.0.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + - @backstage/plugin-techdocs-backend@2.1.6-next.0 + +## e2e-test@0.2.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/create-app@0.7.10-next.0 + - @backstage/errors@1.2.7 + +## @internal/frontend@0.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @internal/scaffolder@0.0.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + +## techdocs-cli-embedded-app@0.2.118-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## yarn-plugin-backstage@0.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/release-manifests@0.0.13 + +## @internal/plugin-todo-list@1.0.49-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @internal/plugin-todo-list-backend@1.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 diff --git a/docs/releases/v1.49.0-next.1-changelog.md b/docs/releases/v1.49.0-next.1-changelog.md new file mode 100644 index 0000000000..7110b3d0d5 --- /dev/null +++ b/docs/releases/v1.49.0-next.1-changelog.md @@ -0,0 +1,1567 @@ +# Release v1.49.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.49.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.49.0-next.1) + +## @backstage/integration@2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code: + + - For Azure DevOps, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead. + - For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead. + - For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead. + - For GitHub, the `getGitHubRequestOptions` function has been removed. + +### Patch Changes + +- 993a598: Fixed Azure integration config schema visibility annotations to use per-field `@visibility secret` instead of `@deepVisibility secret` on parent objects, so that non-secret fields like `clientId`, `tenantId`, `organizations`, and `managedIdentityClientId` are no longer incorrectly marked as secret. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-common@2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated `bitbucket` integration from being registered in the `ScaffolderClient`. Use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Minor Changes + +- f598909: **BREAKING PRODUCERS**: Made `retry`, `listTasks`, `listTemplatingExtensions`, `dryRun`, and `autocomplete` required methods on the `ScaffolderApi` interface. Implementations of `ScaffolderApi` must now provide these methods. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/backend-defaults@0.16.0-next.1 + +### Minor Changes + +- 0e7d8f9: The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution. +- 527cf88: **BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead. + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-dynamic-feature-service@0.8.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/catalog-client@1.14.0-next.1 + +### Minor Changes + +- 972f686: Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for the `query` field in `getEntityFacets` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## @backstage/cli@0.36.0-next.1 + +### Minor Changes + +- b36a60d: **BREAKING**: The `migrate package-exports` command has been removed. Use `repo fix` instead. + +### Patch Changes + +- 0d2d0f2: Internal refactor of CLI modularization, moving individual commands to be implemented with cleye. +- 2fcba39: Internal refactor to move shared utilities into their consuming modules, reducing cross-module dependencies. +- c85ac86: Internal refactor to split `loadCliConfig` into separate implementations for the build and config CLI modules, removing a cross-module dependency. +- 61cb976: Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. +- 825c81d: Internal refactor of CLI command modules. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/repo-tools@0.17.0-next.1 + +### Minor Changes + +- 0fbcf23: Added support for OpenAPI 3.1 to all `schema openapi` commands. The commands now auto-detect the OpenAPI version from the spec file and use the appropriate generator, supporting both OpenAPI 3.0.x and 3.1.x specifications. + +### Patch Changes + +- 426edbe: Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. +- d5779e5: Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + +## @backstage/ui@0.13.0-next.1 + +### Minor Changes + +- 768f09d: **BREAKING**: Simplified the neutral background prop API for container components. The explicit `neutral-1`, `neutral-2`, `neutral-3`, and `neutral-auto` values have been removed from `ProviderBg`. They are replaced by a single `'neutral'` value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. + + **Migration:** + + Replace any explicit `bg="neutral-1"`, `bg="neutral-2"`, `bg="neutral-3"`, or `bg="neutral-auto"` props with `bg="neutral"`. To achieve a specific neutral level in stories or tests, use nested containers — each additional `bg="neutral"` wrapper increments by one level. + + ```tsx + // Before + ... + + // After + + ... + + ``` + + **Affected components:** Box, Flex, Grid, Card, Accordion, Popover, Tooltip, Dialog, Menu + +- b42fcdc: **BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token. + + **Migration:** + + Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells): + + ```diff + - background: var(--bui-bg-popover); + + background: var(--bui-bg-neutral-1); + ``` + + **Affected components:** Popover, Tooltip, Menu, Dialog + +- bd3a76e: **BREAKING**: Data attributes rendered by components are now always lowercase. This affects CSS selectors targeting camelCase data attributes. + + **Migration:** + + Update any custom CSS selectors that target camelCase data attributes to use lowercase instead: + + ```diff + - [data-startCollapsed='true'] { ... } + + [data-startcollapsed='true'] { ... } + ``` + + **Affected components:** SearchField + +- 95702ab: **BREAKING**: Removed deprecated types `ComponentDefinition`, `ClassNamesMap`, `DataAttributeValues`, and `DataAttributesMap` from the public API. These were internal styling infrastructure types that have been replaced by the `defineComponent` system. + + **Migration:** + + Remove any direct usage of these types. Component definitions now use `defineComponent()` and their shapes are not part of the public API contract. + + ```diff + - import type { ComponentDefinition, ClassNamesMap } from '@backstage/ui'; + ``` + + If you were reading `definition.dataAttributes`, use `definition.propDefs` instead — props with `dataAttribute: true` in `propDefs` are the equivalent. + +### Patch Changes + +- 58224d3: Fixed neutral-1 hover & pressed state in light mode. + +- 95702ab: Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + + **Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden + +- 4c2c350: Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + + Affected components: Container + +- d4fa5b4: Fixed tab `matchStrategy` matching to ignore query parameters and hash fragments in tab `href` values. Previously, tabs with query params in their `href` (e.g., `/page?group=foo`) would never show as active since matching compared the full `href` string against `location.pathname` which never includes query params. + + **Affected components:** Tabs, PluginHeader + +- 36987db: Fixed handling of the `style` prop on `Button`, `ButtonIcon`, and `ButtonLink` so that it is now correctly forwarded to the underlying element instead of being silently dropped. + + **Affected components:** Button, ButtonIcon, ButtonLink + +- 95702ab: Fixed Link variant default from `'body'` to `'body-medium'` to match actual CSS selectors. The previous default did not correspond to a valid variant value. + + **Affected components:** Link + +- 9027b10: Fixed scroll overflow in Menu and Select popover content when constrained by viewport height. + + **Affected components:** Menu, Select + +- 4105a78: Merged the internal `PluginHeaderToolbar` component into `PluginHeader`, removing the separate component and its associated types (`PluginHeaderToolbarOwnProps`, `PluginHeaderToolbarProps`) and definition (`PluginHeaderToolbarDefinition`). This is an internal refactor with no changes to the public API of `PluginHeader`. + + **Affected components:** PluginHeader + +- b303857: Fixed `isRequired` prop not being passed to the underlying React Aria field components in TextField, SearchField, and PasswordField. Previously, `isRequired` was consumed locally for the secondary label text but never forwarded, which meant the input elements lacked `aria-required="true"` and React Aria's built-in required validation was not activated. + + **Affected components:** TextField, SearchField, PasswordField + +- cd3cb0f: Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. + +- 36987db: Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + + **Affected components:** Alert, Container, DialogBody, FieldLabel + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-catalog@1.34.0-next.1 + +### Minor Changes + +- 4d58894: Added support for group alias IDs and configurable content ordering on the entity page. Groups can now declare `aliases` so that content targeting an aliased group is included in the group. A new `defaultContentOrder` option (default `title`) controls how content items within each group are sorted, with support for both a page-level default and per-group overrides. + +### Patch Changes + +- 07ba746: Fixed entity page tab groups not respecting the ordering from the `groups` configuration. +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-catalog-backend@3.5.0-next.1 + +### Minor Changes + +- a6b2819: Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 972f686: Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- 72747b4: Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules: + + - `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead + - `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.13.0-next.1 + +### Minor Changes + +- b11c2cd: The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails. + + If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`: + + ```ts + import { createBackendModule } from '@backstage/backend-plugin-api'; + import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org'; + import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github'; + + export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'github-org-custom-transforms', + register(env) { + env.registerInit({ + deps: { + transforms: githubOrgEntityProviderTransformsExtensionPoint, + }, + async init({ transforms }) { + transforms.setUserTransformer(async (item, ctx) => { + const entity = await defaultUserTransformer(item, ctx); + if (entity && item.email) { + entity.spec.profile!.email = item.email; + } + return entity; + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-react@2.1.0-next.1 + +### Minor Changes + +- 4d58894: Added `aliases` and `contentOrder` fields to `EntityContentGroupDefinition`, allowing groups to declare alias IDs and control the sort order of their content items. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-events-backend@0.6.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-scaffolder-backend@3.2.0-next.1 + +### Minor Changes + +- c9b11eb: Added a new `list-scaffolder-tasks` action that allows querying scaffolder tasks with optional ownership filtering and pagination support +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 7695dd2: Added a new `list-scaffolder-actions` action that returns all installed scaffolder actions with their schemas and examples + +### Patch Changes + +- e27bd4e: Removed `@backstage/plugin-scaffolder-backend-module-bitbucket` from `package.json` as the package itself has been deprecated and the code deleted. +- ccc20cf: create scaffolder MCP action to dry run a provided scaffolder template +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-scaffolder-node@0.13.0-next.1 + +### Minor Changes + +- e27bd4e: **BREAKING** Removed deprecated `bitbucket` integration from being used in the `parseRepoUrl` function. It will use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Patch Changes + +- f598909: Added `scaffolderServiceRef` and `ScaffolderService` interface for backend plugins that need to interact with the scaffolder API using `BackstageCredentials` instead of raw tokens. +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-search-backend@2.1.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/backend-test-utils@1.11.1-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/cli-common@0.2.0-next.1 + +### Patch Changes + +- e44b6a9: The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout. +- Updated dependencies + - @backstage/errors@1.2.7 + +## @backstage/cli-node@0.2.19-next.1 + +### Patch Changes + +- 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. +- 3c811bf: Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-compat-api@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## @backstage/create-app@0.7.10-next.1 + +### Patch Changes + +- a9d23c4: Properly support `package.json` `workspaces` field + +- ebd4630: Replace deprecated `workspaces.packages` with `workspaces` in `package.json` + + This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + + ```diff + - "workspaces": { + - "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" + - ] + - }, + ], + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + +## @backstage/dev-utils@1.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + +## @backstage/frontend-dynamic-feature-loader@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/config@1.3.6 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## @backstage/frontend-test-utils@0.5.1-next.1 + +### Patch Changes + +- 479282f: Fixed type inference of `TestApiPair` when using tuple syntax by wrapping `MockWithApiFactory` in `NoInfer`. +- Updated dependencies + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/integration-react@1.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/module-federation-common@0.1.2-next.0 + +### Patch Changes + +- 0cb5646: Fixed the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @techdocs/cli@1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## @backstage/plugin-api-docs@0.13.5-next.1 + +### Patch Changes + +- 30e08df: Added default entity content groups for the API docs entity content tabs. The API definition tab defaults to the `documentation` group and the APIs tab defaults to the `development` group. +- Updated dependencies + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app@0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app-visualizer@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## @backstage/plugin-auth-backend@0.27.1-next.1 + +### Patch Changes + +- 1ccad86: Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-node@0.6.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-azure@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-graph@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-import@0.13.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-node@2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-devtools@0.1.37-next.1 + +### Patch Changes + +- afabb37: Fixed URL encoding of task IDs for the trigger feature (tasks that contained a "/" in their ID were not triggered) +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-events-backend-module-github@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-home@0.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## @backstage/plugin-kubernetes@0.12.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-backend@0.21.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-mui-to-bui@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + +## @backstage/plugin-notifications-backend-module-email@0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + +## @backstage/plugin-notifications-node@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-org@0.6.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-org-react@0.1.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-scaffolder@1.35.5-next.1 + +### Patch Changes + +- e27bd4e: Removed check for deprecated `bitbucket` integration from `repoPickerValidation` function used by the `RepoUrlPicker`, it now validates the `bitbucketServer` and `bitbucketCloud` integrations instead. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.4-next.1 + +### Patch Changes + +- 0c1726a: Added new `gitlab:group:access` scaffolder action to add or remove users and groups as members of GitLab groups. The action supports specifying members via `userIds` and/or `groupIds` array parameters, configurable access levels (Guest, Reporter, Developer, Maintainer, Owner), and defaults to the 'add' action when not specified. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-react@1.19.8-next.1 + +### Patch Changes + +- 004b5c1: Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` as alpha exports. +- f598909: Added `scaffolderApiMock` test utility, exported from `@backstage/plugin-scaffolder-react/testUtils`. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-search@1.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + +## @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-techdocs@1.17.1-next.1 + +### Patch Changes + +- 30e08df: Added `documentation` as the default entity content group for the TechDocs entity content tab. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-backend@2.1.6-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-node@1.14.3-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings@0.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## example-app@0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/plugin-app-visualizer@0.2.1-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## example-app-legacy@0.2.119-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-mui-to-bui@0.2.5-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## example-backend@0.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@3.2.0-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-search-backend@2.1.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + +## techdocs-cli-embedded-app@0.2.118-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 diff --git a/docs/releases/v1.49.0-next.2-changelog.md b/docs/releases/v1.49.0-next.2-changelog.md new file mode 100644 index 0000000000..b6442fa4b0 --- /dev/null +++ b/docs/releases/v1.49.0-next.2-changelog.md @@ -0,0 +1,1826 @@ +# Release v1.49.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.49.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.49.0-next.2) + +## @backstage/plugin-catalog@2.0.0-next.2 + +### Major Changes + +- 5fc35bb: Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + + **BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + + **Migration:** + + Simply delete the obsolete `variant` and `gridSizes` props, e.g: + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @backstage/backend-app-api@1.6.0-next.1 + +### Minor Changes + +- 545557a: Registration errors should be forwarded as BackendStartupResult + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/backend-plugin-api@1.8.0-next.1 + +### Minor Changes + +- 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/catalog-client@1.14.0-next.2 + +### Minor Changes + +- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error. + +## @backstage/cli@0.36.0-next.2 + +### Minor Changes + +- d0f4cd2: Added new `auth` command group for authenticating the CLI with Backstage instances using OAuth 2.0 with a pre-registered client metadata document. Commands include `login`, `logout`, `list`, `show`, `print-token`, and `select` for managing multiple authenticated instances. + +### Patch Changes + +- a4e5902: Internal refactor of the CLI command registration +- ff4a45a: Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. Several camelCase CLI flags have been deprecated in favor of their kebab-case equivalents (e.g. `--successCache` → `--success-cache`). The old camelCase forms still work but will now log a deprecation warning. Please update any scripts or CI configurations to use the kebab-case versions. +- 4a75544: Updated dependency `react-refresh` to `^0.18.0`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + - @backstage/integration@2.0.0-next.2 + +## @backstage/frontend-app-api@0.16.0-next.1 + +### Minor Changes + +- 92af1ae: **BREAKING**: Removed the `allowUnknownExtensionConfig` option from `createSpecializedApp`. This flag had no effect and was a no-op, so no behavioral changes are expected. + +### Patch Changes + +- 0452d02: Add optional `description` field to plugin-level feature flags. +- dab6c46: Added the `ExtensionFactoryMiddleware` type as a public export. +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-defaults@0.5.0-next.1 + +## @backstage/frontend-defaults@0.5.0-next.1 + +### Minor Changes + +- 92af1ae: **BREAKING**: Removed the `allowUnknownExtensionConfig` option from `createApp`. This flag had no effect and was a no-op, so no behavioral changes are expected. +- 33de79d: **BREAKING**: Removed the deprecated `createPublicSignInApp` function. Use `createApp` from `@backstage/frontend-defaults` with `appModulePublicSignIn` from `@backstage/plugin-app/alpha` instead. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app@0.4.1-next.2 + +## @backstage/frontend-plugin-api@0.15.0-next.1 + +### Minor Changes + +- 6573901: **BREAKING**: Removed the deprecated `AnyExtensionDataRef` type. Use `ExtensionDataRef` without type parameters instead. +- a9440f0: **BREAKING**: Simplified the `ExtensionAttachTo` type to only support a single attachment target. The array form for attaching to multiple extension points has been removed. Also removed the deprecated `ExtensionAttachToSpec` type alias. + +### Patch Changes + +- 8a3a906: Deprecated `NavItemBlueprint`. Nav items are now automatically inferred from `PageBlueprint` extensions based on their `title` and `icon` params. +- b15a685: Deprecated `withApis`, use the `withApis` export from `@backstage/core-compat-api` instead. +- 0452d02: Add optional `description` field to plugin-level feature flags. +- 1bec049: Fixed inconsistent `JSX.Element` type reference in the `DialogApiDialog.update` method signature. +- 2c383b5: Deprecated `AnalyticsImplementationBlueprint` and `AnalyticsImplementationFactory` in favor of the exports from `@backstage/plugin-app-react`. +- dab6c46: Deprecated the `ExtensionFactoryMiddleware` type, which has been moved to `@backstage/frontend-app-api`. +- d0206c4: Removed the deprecated `defaultPath` migration helper from `PageBlueprint` params. +- edb872c: Renamed the `PageTab` type to `PageLayoutTab`. The old `PageTab` name is now a deprecated type alias. +- fe848e0: Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context. + +## @backstage/plugin-catalog-backend@3.5.0-next.2 + +### Minor Changes + +- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error. + +### Patch Changes + +- 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-catalog-graph@0.6.0-next.2 + +### Minor Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-catalog-react@2.1.0-next.2 + +### Minor Changes + +- d14b6e0: Exported `useEntityRefLink` hook that returns a function for generating entity page URLs from entity references. +- c6080eb: Added `EntityInfoCard` component to `@backstage/plugin-catalog-react` as a BUI-based card wrapper for entity page cards. + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-test-utils@0.5.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.4.0-next.1 + +### Minor Changes + +- cd62d78: **BREAKING**: Only send direct messages to user entity recipients. Notifications sent to non-user entities no longer send Slack direct messages to resolved users. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + +## @backstage/plugin-org@0.7.0-next.2 + +### Minor Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +- 5fc35bb: Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + + **BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + + **Migration:** + + Simply delete the obsolete `variant` and `gridSizes` props, e.g: + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-scaffolder-backend@3.2.0-next.2 + +### Minor Changes + +- e8736ea: Added secrets schema validation for task creation, retry, and dry-run endpoints. When a template defines `spec.secrets.schema`, the API validates provided secrets against the schema and returns a `400` error if validation fails. + +### Patch Changes + +- 30ff981: Fixed a security vulnerability where secrets could bypass log redaction when transformed through Nunjucks filters in scaffolder templates. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-common@2.0.0-next.2 + +### Minor Changes + +- e8736ea: Added an optional `secrets` field to `TemplateEntityV1beta3` for configuring secrets validation. The schema for validating secrets is defined under `secrets.schema` as a JSON Schema object. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.2 + +## @backstage/plugin-scaffolder-react@1.20.0-next.2 + +### Minor Changes + +- 470f72d: The `LogViewer` component from `@backstage/core-components` now supports downloading logs if a callback is passed to `onDownloadLogs` + +### Patch Changes + +- bd31ddd: Updated dependency `flatted` to `3.3.4`. +- Updated dependencies + - @backstage/frontend-test-utils@0.5.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + +## @backstage/backend-defaults@0.16.0-next.2 + +### Patch Changes + +- 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available. +- 5fcbef2: Updated dependency `express-rate-limit` to `^8.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/backend-app-api@1.6.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/backend-dynamic-feature-service@0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.43-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-backend@0.6.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/backend-openapi-utils@0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/backend-test-utils@1.11.1-next.2 + +### Patch Changes + +- 164711a: Added `cancelTask` to `MockSchedulerService` and mock scheduler service factory. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/backend-app-api@1.6.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/cli-common@0.2.0-next.2 + +### Patch Changes + +- 9361965: Fixed `runCheck` to ignore stdio of the spawned process, preventing unwanted output from leaking to the terminal. + +## @backstage/core-app-api@1.19.6-next.1 + +### Patch Changes + +- 12d8afe: Added `BUIProvider` from `@backstage/ui` to the app shell provider tree, enabling BUI components to fire analytics events through the Backstage analytics system. +- 0452d02: Add optional `description` field to plugin-level feature flags. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + +## @backstage/core-compat-api@0.5.9-next.2 + +### Patch Changes + +- b15a685: Added `withApis`, which is a Higher-Order Component for providing APIs as props to a component via `useApiHolder`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/plugin-app-react@0.2.1-next.1 + +## @backstage/core-components@0.18.8-next.1 + +### Patch Changes + +- 8b1a847: Fixed Table component layout when both `filters` and `title` props are used together. The filter controls now use a dedicated CSS class (`filterControls`) instead of incorrectly reusing the root container class. +- 470f72d: The `LogViewer` component from `@backstage/core-components` now supports downloading logs if a callback is passed to `onDownloadLogs` +- Updated dependencies + - @backstage/core-plugin-api@1.12.4-next.1 + +## @backstage/core-plugin-api@1.12.4-next.1 + +### Patch Changes + +- 0452d02: Add optional `description` field to plugin-level feature flags. +- fe848e0: Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## @backstage/create-app@0.7.10-next.2 + +### Patch Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + +## @backstage/frontend-dynamic-feature-loader@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## @backstage/frontend-test-utils@0.5.1-next.2 + +### Patch Changes + +- b56f573: Deprecated standalone mock API exports in favor of the `mockApis` namespace. This includes the mock classes (`MockAlertApi`, `MockAnalyticsApi`, `MockConfigApi`, `MockErrorApi`, `MockFetchApi`, `MockFeatureFlagsApi`, `MockPermissionApi`, `MockStorageApi`, `MockTranslationApi`), their option types (`MockErrorApiOptions`, `MockFeatureFlagsApiOptions`), and the `ErrorWithContext` type. `MockFetchApiOptions` is kept as a non-deprecated export. Use the `mockApis` namespace instead, for example `mockApis.alert()` or `mockApis.alert.mock()`. +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/plugin-app@0.4.1-next.2 + +## @backstage/integration@2.0.0-next.2 + +### Patch Changes + +- 1513a0b: Fixed a security vulnerability where path traversal sequences in SCM URLs could be used to access unintended API endpoints using server-side integration credentials. + +## @backstage/repo-tools@0.17.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/cli-common@0.2.0-next.2 + +## @backstage/ui@0.13.0-next.2 + +### Patch Changes + +- db92751: Added interactive support to the `Card` component. Pass `onPress` to make the entire card surface pressable, or `href` to make it navigate to a URL. A transparent overlay handles the interaction while nested buttons and links remain independently clickable. + +- 12d8afe: Added analytics capabilities to the component library. Components with navigation behavior (Link, ButtonLink, Tab, MenuItem, Tag, Row) now fire analytics events on click when a `BUIProvider` is present. + + New exports: `BUIProvider`, `useAnalytics`, `getNodeText`, and associated types (`AnalyticsTracker`, `UseAnalyticsFn`, `BUIProviderProps`, `AnalyticsEventAttributes`). + + Components with analytics support now accept a `noTrack` prop to suppress event firing. + + **Affected components:** Link, ButtonLink, Tab, MenuItem, Tag, Row + +- 430d5ed: Fixed interactive cards so that CardBody can scroll when the card has a constrained height. Previously, the overlay element blocked scroll events. + + **Affected components:** Card + +- ad7c883: Deprecated the `HeaderPage` component name in favor of `Header`. The old `HeaderPage`, `HeaderPageProps`, `HeaderPageOwnProps`, `HeaderPageBreadcrumb`, and `HeaderPageDefinition` exports are still available as deprecated aliases. + +- f42f4cc: Fixed Table column headers overflowing and wrapping when there is not enough space. Headers now truncate with ellipsis instead. + + **Affected components:** Table + +- fbd5c5a: Fixed Table rows showing a pointer cursor when not interactive. Rows now only show `cursor: pointer` when they have an `href`, are selectable, or are pressable. + + **Affected components:** Table + +- 7960d54: Added support for native HTML div attributes on the `Flex`, `Grid`, and `Grid.Item` components. + + **Affected components:** Flex, Grid, Grid.Item + +- 12d8afe: Fixed MenuItem `onAction` prop ordering so user-provided `onAction` handlers are chained rather than silently overwritten. + +- bb66b86: The `Select` trigger now automatically adapts its background colour based on the parent background context. + + **Affected components:** Select + +- 934ac03: `SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token. + + **Affected components:** SearchField, TextField + +## @backstage/plugin-api-docs@0.13.5-next.2 + +### Patch Changes + +- ca277ef: Updated dependency `graphiql` to `3.9.0` to address security vulnerability in `markdown-it` package. + Updated dependency `@graphiql/react` to `0.29.0` to match the version used by `graphiql`. + Moved dependency `graphql-config` to `devDependencies` as it is needed only for types. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-catalog@2.0.0-next.2 + +## @backstage/plugin-app@0.4.1-next.2 + +### Patch Changes + +- 12d8afe: Added `BUIProvider` from `@backstage/ui` to the app root, enabling BUI components to fire analytics events through the Backstage analytics system. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + +## @backstage/plugin-app-backend@0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-app-node@0.1.43-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-app-node@0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-app-react@0.2.1-next.1 + +### Patch Changes + +- 2c383b5: Added `AnalyticsImplementationBlueprint` and `AnalyticsImplementationFactory`, migrated from `@backstage/frontend-plugin-api`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## @backstage/plugin-app-visualizer@0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-auth@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-auth-backend@0.27.1-next.2 + +### Patch Changes + +- d0f4cd2: Added optional client metadata document endpoint at `/.well-known/oauth-client/cli.json` relative to the auth backend base URL for CLI authentication. Enabled when `auth.experimentalClientIdMetadataDocuments.enabled` is set to `true`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-auth-node@0.6.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + +## @backstage/plugin-catalog-backend-module-aws@0.4.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-azure@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-github@0.13.0-next.2 + +### Patch Changes + +- 106d1b2: Added a `defaultUserTransformer.useVerifiedEmails` config option for the `githubOrg` provider. When set to `true`, the default user transformer prefers organization verified domain emails over the user's public GitHub email. Defaults to `false`, which uses only the public GitHub email. + + This option has no effect when a custom user transformer is set via the `githubOrgEntityProviderTransformsExtensionPoint`. + + ```yaml + catalog: + providers: + githubOrg: + production: + githubUrl: https://github.com + orgs: + - my-org + defaultUserTransformer: + useVerifiedEmails: true + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.20-next.2 + +### Patch Changes + +- 106d1b2: Added a `defaultUserTransformer.useVerifiedEmails` config option for the `githubOrg` provider. When set to `true`, the default user transformer prefers organization verified domain emails over the user's public GitHub email. Defaults to `false`, which uses only the public GitHub email. + + This option has no effect when a custom user transformer is set via the `githubOrgEntityProviderTransformsExtensionPoint`. + + ```yaml + catalog: + providers: + githubOrg: + production: + githubUrl: https://github.com + orgs: + - my-org + defaultUserTransformer: + useVerifiedEmails: true + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.13.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-logs@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.9.1-next.1 + +### Patch Changes + +- 97eaecf: Fixed scheduler task remaining stuck in running state after pod termination by propagating AbortSignal into MicrosoftGraphOrgEntityProvider.read() +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-catalog-import@0.13.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-catalog-node@2.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-devtools-react@0.1.2-next.1 + +## @backstage/plugin-devtools@0.1.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-devtools-react@0.1.2-next.1 + +## @backstage/plugin-devtools-backend@0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-devtools-react@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## @backstage/plugin-events-backend@0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-azure@0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-github@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-backend-module-kafka@0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-events-node@0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-gateway-backend@1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-home@0.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-home-react@0.1.36-next.1 + +## @backstage/plugin-home-react@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-kubernetes@0.12.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-kubernetes-backend@0.21.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-kubernetes-node@0.4.2-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-kubernetes-node@0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-mcp-actions-backend@0.1.10-next.2 + +### Patch Changes + +- c74b697: Added support for splitting MCP actions into multiple servers via `mcpActions.servers` configuration. Each server gets its own endpoint at `/api/mcp-actions/v1/{key}` with actions scoped using include/exclude filter rules. Tool names are now namespaced with the plugin ID by default, configurable via `mcpActions.namespacedToolNames`. When `mcpActions.servers` is not configured, the plugin continues to serve a single server at `/api/mcp-actions/v1`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-mui-to-bui@0.2.5-next.2 + +### Patch Changes + +- ad7c883: Updated the MUI to BUI theme converter page to use the renamed `Header` component from `@backstage/ui`. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## @backstage/plugin-notifications@0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-notifications-backend@0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + +## @backstage/plugin-notifications-node@0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## @backstage/plugin-permission-backend@0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## @backstage/plugin-permission-node@0.10.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## @backstage/plugin-proxy-backend@0.6.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-proxy-node@0.1.13-next.1 + +## @backstage/plugin-proxy-node@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-scaffolder@1.35.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.2 + +### Patch Changes + +- b2591f6: Fixed environment `waitTime` description incorrectly asking for milliseconds instead of minutes. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.2 + +## @backstage/plugin-scaffolder-node@0.13.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## @backstage/plugin-search@1.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-search-react@1.10.5-next.1 + +## @backstage/plugin-search-backend@2.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-catalog@0.3.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-explore@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-pg@0.5.53-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-techdocs-node@1.14.4-next.2 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## @backstage/plugin-search-backend-node@1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## @backstage/plugin-search-react@1.10.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-signals@0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-signals-backend@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## @backstage/plugin-signals-node@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## @backstage/plugin-techdocs@1.17.1-next.2 + +### Patch Changes + +- 9795d30: chore(deps): bump `dompurify` from 3.3.1 to 3.3.2 +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @backstage/plugin-techdocs-backend@2.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-techdocs-node@1.14.4-next.2 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @backstage/plugin-techdocs-node@1.14.4-next.2 + +### Patch Changes + +- e96f6d9: Removed `INHERIT` from the `ALLOWED_MKDOCS_KEYS` set to address a security concern with MkDocs configuration inheritance. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + +## @backstage/plugin-techdocs-react@1.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-user-settings@0.9.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## @backstage/plugin-user-settings-backend@0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## example-app@0.0.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-org@0.7.0-next.2 + - @backstage/plugin-catalog-graph@0.6.0-next.2 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/plugin-app@0.4.1-next.2 + - @backstage/frontend-defaults@0.5.0-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-api-docs@0.13.5-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-app-visualizer@0.2.1-next.2 + - @backstage/plugin-auth@0.1.6-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.1 + - @backstage/plugin-devtools@0.1.37-next.2 + - @backstage/plugin-home@0.9.3-next.2 + - @backstage/plugin-home-react@0.1.36-next.1 + - @backstage/plugin-kubernetes@0.12.17-next.2 + - @backstage/plugin-notifications@0.5.15-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.2 + - @backstage/plugin-search@1.6.2-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-signals@0.0.29-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + - @backstage/plugin-user-settings@0.9.1-next.2 + +## app-example-plugin@0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + +## example-app-legacy@0.2.119-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-org@0.7.0-next.2 + - @backstage/plugin-catalog-graph@0.6.0-next.2 + - @backstage/plugin-mui-to-bui@0.2.5-next.2 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-api-docs@0.13.5-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-catalog-import@0.13.11-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.1 + - @backstage/plugin-devtools@0.1.37-next.2 + - @backstage/plugin-home@0.9.3-next.2 + - @backstage/plugin-home-react@0.1.36-next.1 + - @backstage/plugin-kubernetes@0.12.17-next.2 + - @backstage/plugin-notifications@0.5.15-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.2 + - @backstage/plugin-search@1.6.2-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-signals@0.0.29-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + - @backstage/plugin-user-settings@0.9.1-next.2 + +## example-backend@0.0.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-scaffolder-backend@3.2.0-next.2 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.2 + - @backstage/plugin-app-backend@0.5.12-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.1 + - @backstage/plugin-devtools-backend@0.5.15-next.1 + - @backstage/plugin-events-backend@0.6.0-next.2 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.2 + - @backstage/plugin-notifications-backend@0.6.3-next.1 + - @backstage/plugin-permission-backend@0.7.10-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-proxy-backend@0.6.11-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.2 + - @backstage/plugin-search-backend@2.1.0-next.2 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + - @backstage/plugin-signals-backend@0.3.13-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.2 + +## @internal/frontend@0.0.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## @internal/scaffolder@0.0.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + +## techdocs-cli-embedded-app@0.2.118-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/frontend-defaults@0.5.0-next.1 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## @internal/plugin-todo-list-backend@1.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index 2c7826a149..c7bc468632 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -294,6 +294,20 @@ will be set up that listens to the protocol, host and port set by `app.baseUrl` in the configuration. If needed it is also possible to override the listening options through the `app.listen` configuration. +For frontend plugin packages using the new frontend system, the recommended way to +set up the `dev/index` entry point is to use the `createDevApp` helper from +`@backstage/frontend-dev-utils`. It creates and renders a minimal Backstage app +with your plugin loaded: + +```tsx title="in dev/index.ts" +import { createDevApp } from '@backstage/frontend-dev-utils'; +import myPlugin from '../src'; + +createDevApp({ features: [myPlugin] }); +``` + +For the legacy frontend system, the `@backstage/dev-utils` package provides equivalent helpers. + The frontend development bundling is currently based on [Webpack](https://webpack.js.org/) and [Webpack Dev Server](https://webpack.js.org/configuration/dev-server/). The diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 2a09cb1b11..2ed0e91891 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -24,6 +24,7 @@ repo [command] Command that run across an entire package [command] Lifecycle scripts for individual packages migrate [command] Migration utilities versions:bump [options] Bump Backstage packages to the latest versions +translations [command] Translation message management clean Delete cache directories [DEPRECATED] build-workspace [packages...] Builds a temporary dist workspace from the provided packages @@ -203,6 +204,129 @@ Options: --module-federation Build a package as a module federation remote. Applies to frontend plugin packages only. ``` +## package bundle + +:::caution Experimental +This command is experimental and may receive breaking changes in future releases +without a deprecation period. It is hidden from the main `--help` output. +::: + +Bundle a plugin for dynamic loading. This creates a self-contained plugin +package that can be deployed independently and loaded dynamically by a Backstage +application. Supports both backend and frontend plugins. + +Unlike regular builds, the bundle command: + +- Creates a fully self-contained plugin deliverable + +- Produces module federation assets (frontend) or includes plugin dependencies in the plugin's private `node_modules`, building and packing (with `yarn pack`) the local `workspace:^` dependencies first (backend). +- Generates a config schema from plugin-related packages only. +- Validates that the plugin exports valid dynamic loading entry points (backend only) + +### Usage + +```bash +# Bundle the current package (output: ./bundle/) +yarn backstage-cli package bundle + +# Bundle to a specific directory (output: ../dynamic-plugins//) +yarn backstage-cli package bundle --output-destination ../dynamic-plugins + +# Override the bundle subdirectory name +yarn backstage-cli package bundle --output-name my-plugin-bundle + +# Clean output before bundling +yarn backstage-cli package bundle --clean + +# Skip building for the plugin and its local dependencies +yarn backstage-cli package bundle --no-build + +# Skip dependency installation and entrypoint validation +yarn backstage-cli package bundle --no-install + +# Stream detailed output from build, pack, and install steps +yarn backstage-cli package bundle --verbose + +# Use a pre-built dist workspace for batch bundling. +# First, create the workspace with: +# backstage-cli build-workspace [packages...] --alwaysPack +# Then pass as --pre-packed-dir: +yarn backstage-cli package bundle --pre-packed-dir ../dist-workspace +``` + +### Options + +```text +Usage: backstage-cli package bundle [options] + +Bundle a plugin for dynamic loading + +Options: + --output-destination Directory in which the bundle subdirectory is created. + Defaults to the current package directory. + --output-name Name of the bundle subdirectory. Defaults to "bundle" when + output stays in the package directory, or to the mangled + package name (e.g. myorg-plugin-foo) when + --output-destination is specified. + --clean Clean the output directory before bundling + --no-build Skip building packages (assumes they are already built) + --no-install Skip dependency installation and entrypoint validation. + --verbose Stream detailed output from internal steps (build, pack, + install) to the console. Without this flag, output is + captured to per-step log files and only shown on error. + --pre-packed-dir Path to a pre-built dist workspace (from + build-workspace --alwaysPack). Skips local dependency + packing and uses pre-packed packages directly. For frontend + plugins, this also enables yarn.lock generation for SBOM. +``` + +### Output Contract + +The bundle output is a directory that can be deployed as a standalone unit. +Consumers of the bundle (such as `@backstage/backend-dynamic-feature-service` +or `@backstage/frontend-dynamic-feature-loader`) can rely on the following +guarantees: + +**All bundles:** + +- A `package.json` at the bundle root with entry points configured for dynamic + loading. The `backstage.role` and `files` fields are preserved from the source package. +- A `dist/` directory containing the built plugin code. +- A `dist/.config-schema.json` file (when any config schemas apply) containing + gathered schemas from the plugin, its local workspace dependencies, and + third-party dependencies. Schemas from unrelated Backstage packages are excluded. +- No `scripts` or `devDependencies` in `package.json`. + +**Backend plugins** (`backend-plugin`, `backend-plugin-module`): + +- A `node_modules/` directory with all production dependencies (including local + workspace dependencies), pinned to their exact versions from the source lockfile. +- `bundleDependencies` is set to `true` in `package.json`. + +**Frontend plugins** (`frontend-plugin`, `frontend-plugin-module`): + +- `main` points to `dist/remoteEntry.js` (the Module Federation remote entry). +- `types` points to `dist/@mf-types/index.d.ts` when type declarations are + available. +- No embedded `node_modules/` directory. + +### Environment Variables + +The bundle command supports the same environment variables as the Backstage yarn plugin +for resolving `backstage:^` version specifiers: + +- `BACKSTAGE_MANIFEST_FILE`: Path to a local manifest file (for offline usage) +- `BACKSTAGE_VERSIONS_BASE_URL`: Custom base URL for fetching release manifests + +### Supported Package Roles + +The bundle command supports packages with the following roles: + +- `backend-plugin` +- `backend-plugin-module` +- `frontend-plugin` +- `frontend-plugin-module` + ## package lint Lint a package. In addition to the default `eslint` behavior, this command will @@ -413,9 +537,17 @@ package. This essentially calls `yarn pack` in each included package and unpacks the resulting archive in the target `workspace-dir`. ```text -Usage: backstage-cli build-workspace [options] +Usage: backstage-cli build-workspace [options] [packages...] + +Options: + --alwaysPack Force workspace output to be a result of running `yarn pack` on + each package (warning: very slow) ``` +When `--alwaysPack` is used, the output directory can be passed to +`backstage-cli package bundle --pre-packed-dir` to speed up batch bundling of +multiple plugins from the same monorepo. + ## create-github-app Creates a GitHub App in your GitHub organization. This is an alternative to @@ -429,6 +561,73 @@ YAML file that can be referenced in the GitHub integration configuration. Usage: backstage-cli create-github-app ``` +## translations export + +Export translation messages from an app and all of its frontend plugins to JSON +files. This command must be run from within a package directory (e.g. +`packages/app`), not from the repository root. + +The command discovers all `TranslationRef` definitions in the dependency tree, +extracts their default messages using the TypeScript type system, and writes +them as JSON files along with a manifest. + +For more details on the translation workflow, see the +[Internationalization](../../plugins/internationalization.md) documentation. + +```text +Usage: backstage-cli translations export [options] + +Options: + --output Output directory for exported messages and manifest (default: "translations") + --pattern File path pattern for message files relative to the output + directory, with {id} and {lang} placeholders + (default: "messages/{id}.{lang}.json") + -h, --help display help for command +``` + +### Examples + +Export translations with default settings: + +```bash +cd packages/app +yarn backstage-cli translations export +``` + +Export with language-based directory grouping: + +```bash +yarn backstage-cli translations export --pattern '{lang}/{id}.json' +``` + +## translations import + +Generate translation resource wiring code from translated JSON files. Reads the +manifest and translated message files produced by `translations export`, and +generates a TypeScript module that creates `TranslationResource` objects for each +translated ref. + +The file pattern used during export is stored in the manifest and automatically +used by the import command. + +```text +Usage: backstage-cli translations import [options] + +Options: + --input Input directory containing the manifest and translated message files (default: "translations") + --output Output path for the generated wiring module (default: "src/translations/resources.ts") + -h, --help display help for command +``` + +### Examples + +Generate wiring code with default settings: + +```bash +cd packages/app +yarn backstage-cli translations import +``` + ## info Outputs debug information which is useful when opening an issue. Outputs system diff --git a/docs/tutorials/enable-public-entry.md b/docs/tutorials/enable-public-entry.md index e430768785..a1321dca05 100644 --- a/docs/tutorials/enable-public-entry.md +++ b/docs/tutorials/enable-public-entry.md @@ -107,10 +107,11 @@ If your app uses the new frontend system, you can still use the public entry poi ```tsx title="in packages/app/src/index-public-experimental.tsx" import ReactDOM from 'react-dom/client'; import { signInPageModule } from './overrides/SignInPage'; -import { createPublicSignInApp } from '@backstage/frontend-defaults'; +import { appModulePublicSignIn } from '@backstage/plugin-app/alpha'; +import { createApp } from '@backstage/frontend-defaults'; -const app = createPublicSignInApp({ - features: [signInPageModule], +const app = createApp({ + features: [signInPageModule, appModulePublicSignIn], }); ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot()); diff --git a/microsite/README.md b/microsite/README.md index 62a28fb6a3..42b39acfa4 100644 --- a/microsite/README.md +++ b/microsite/README.md @@ -13,6 +13,7 @@ This website was created with [Docusaurus](https://docusaurus.io/). - [Editing Content](#editing-content) - [Adding Content](#adding-content) - [Full Documentation](#full-documentation) +- [How to patch the stable docs](#patching-the-docs) # Getting Started @@ -240,3 +241,7 @@ Full documentation can be found on the [Docusaurus website](https://docusaurus.i A feedback widget is provided by the `docusaurus-pushfeedback` plugin, which connects to the https://pushfeedback.com service. Styling of the button is configured in `microsite/src/theme/customTheme.scss`, while the plugin itself is configured in `microsite/docusaurus.config.js`. Feedback submissions are connected to an account owned by @Rugvip and are available on request, reach out to @Rugvip on [Discord](https://discord.gg/backstage-687207715902193673). + +# Patching the docs + +To patch the documentation for the stable version, you can submit a PR to the correct `patch/v` branch. For example, if `1.48.0` is the stable version, you would submit a fix to the branch `patch/v1.48.0`. Once your fix is in, you can trigger the `deploy_microsite` GH action (or wait until the next run on master) and you should see the fix on the stable version at https://backstage.io/docs. diff --git a/microsite/blog/2020-03-18-what-is-backstage.mdx b/microsite/blog/2020-03-18-what-is-backstage.mdx index caac489ca1..2fd04768d6 100644 --- a/microsite/blog/2020-03-18-what-is-backstage.mdx +++ b/microsite/blog/2020-03-18-what-is-backstage.mdx @@ -36,7 +36,7 @@ Our internal installation of Backstage has over 100 different integrations — w 3. Centralised technical documentation 4. Review performance of your team’s mobile features -These are just a few examples. Expect us to continue providing examples of how Backstage is used inside Spotify while we build out more end-2-end use-cases in the open. +These are just a few examples. Expect us to continue providing examples of how Backstage is used inside Spotify while we build out more end-to-end use-cases in the open. ### 1. Creating a new microservice diff --git a/microsite/blog/2023-04-26-kubecon-eu-2023.mdx b/microsite/blog/2023-04-26-kubecon-eu-2023.mdx index 8f369c2d14..983e546bd2 100644 --- a/microsite/blog/2023-04-26-kubecon-eu-2023.mdx +++ b/microsite/blog/2023-04-26-kubecon-eu-2023.mdx @@ -1,7 +1,7 @@ --- title: 'ICYMI: KubeCon EU 2023' author: Bailey Brooks, Spotify -authorURL: http://github.com/bailey +authorURL: https://github.com/bailey authorImageURL: https://pbs.twimg.com/profile_images/1477424303192694785/qCfN6XWW_400x400.jpg --- diff --git a/microsite/blog/2023-06-21-switching-out-sandbox.mdx b/microsite/blog/2023-06-21-switching-out-sandbox.mdx index f86cb126a3..920a1f54d8 100644 --- a/microsite/blog/2023-06-21-switching-out-sandbox.mdx +++ b/microsite/blog/2023-06-21-switching-out-sandbox.mdx @@ -71,4 +71,4 @@ This change comes with the v1.15.0 release of Backstage that was released yester For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). -If you have any further questions you can either reach out to us in the [Community Discord](https://discord.gg/backstage-687207715902193673), or in the [office hours](https://info.backstage.spotify.com/office-hours). +If you have any further questions you can either reach out to us in the [Community Discord](https://discord.gg/backstage-687207715902193673), or in the [office hours](https://spoti.fi/backstageofficehours). diff --git a/microsite/blog/2023-09-29-chicago-traiding-company-adopter-spotlight.mdx b/microsite/blog/2023-09-29-chicago-traiding-company-adopter-spotlight.mdx index bd479a6a86..d7297ad145 100644 --- a/microsite/blog/2023-09-29-chicago-traiding-company-adopter-spotlight.mdx +++ b/microsite/blog/2023-09-29-chicago-traiding-company-adopter-spotlight.mdx @@ -21,19 +21,19 @@ After exploring several service routes from managed to in-house/owned, CTC opted The CTC DevOps team created a small special interest group to champion the developer portal build and quickly delved into the big issues impacting their developer experience. From there, the team began creating tasks and templates within Backstage. -To start, Scott worked with end-user teams outside DevOps early in the process of documenting migration to the new DevOps Kubernetes platform services. When Scott wrote the onboarding documentation, he made it a point to pair with a developer on another team that would be using it. This approach provided him with a quality sounding board, great instantaneous feedback, and a sort of "beta developer" to test V1 documentation clarity., A few weeks later, another colleague on the same end-user team followed the documentation and she didn't reach out to Scott at all. +To start, Scott worked with end-user teams outside DevOps early in the process of documenting migration to the new DevOps Kubernetes platform services. When Scott wrote the onboarding documentation, he made it a point to pair with a developer on another team that would be using it. This approach provided him with a quality sounding board, great instantaneous feedback, and a sort of "beta developer" to test V1 documentation clarity. A few weeks later, another colleague on the same end-user team followed the documentation and she didn't reach out to Scott at all. After this early win, the team was ready for broad distribution. Partnering with the senior leadership team, Scott's team began onboarding teams to the new DevOps Kubernetes platform using the templates his team created in Backstage. At CTC, their Backstage instance is set up to automatically scan for deployments in Kubernetes; so if you're in Kubernetes, you're onboarded to Backstage. -"Backstage made onboarding [to the new DevOps platform] not scary,"" Scott said. "Because now — all of a sudden — you have this recipe for how to do it, you don't have to jump through these hoops, the templates are there and ready for you. You choose what you want based on these templates we have available, and you're off and running on your own."" +"Backstage made onboarding [to the new DevOps platform] not scary," Scott said. "Because now — all of a sudden — you have this recipe for how to do it, you don't have to jump through these hoops, the templates are there and ready for you. You choose what you want based on these templates we have available, and you're off and running on your own." Scott's team monitored progress against their onboarding goals partially by how often the DevOps team was getting pinged for support and found that it has made support smoother. They can easily refer people to templates and repo standards instead of making them create something ad hoc. -"It's really been cool seeing how that progressed. I even have a few teams that didn't talk to DevOps at all. They used a [template] and they have a service ready. We were actually pretty excited about that, because that means now we have a scalable tool able to onboard others onto our platform."" said Scott. +"It's really been cool seeing how that progressed. I even have a few teams that didn't talk to DevOps at all. They used a [template] and they have a service ready. We were actually pretty excited about that, because that means now we have a scalable tool able to onboard others onto our platform," said Scott. -Another exciting win for CTC was template additions generated outside the initial toolkit, which has eased the burden substantially for the DevOps team. They were able to create new templates to deploy against best practices such as a template for creating a brand-new Terraform module and Git repo or — Scott's favorite — ​​a template that creates a Java repository along with Jenkins jobs for CI and the Flux CD config for deployment. +Another exciting win for CTC was template additions generated outside the initial toolkit, which has eased the burden substantially for the DevOps team. They were able to create new templates to deploy against best practices such as a template for creating a brand-new Terraform module and Git repo or — Scott's favorite — a template that creates a Java repository along with Jenkins jobs for CI and the Flux CD config for deployment. -"The template builds and auto-deploys a Docker image, so if you make a change to your mainline branch, then it will automatically build a new Docker image and deploy it to a Kubernetes cluster."" Scott said. "So within basically 10 minutes of you filling out a form, you actually have something deployed out to a Kubernetes cluster."" +"The template builds and auto-deploys a Docker image, so if you make a change to your mainline branch, then it will automatically build a new Docker image and deploy it to a Kubernetes cluster," Scott said. "So within basically 10 minutes of you filling out a form, you actually have something deployed out to a Kubernetes cluster." This process eased flows for both end-user devs and DevOps teams overall. @@ -49,12 +49,12 @@ For CTC, the journey with Backstage has just begun. They're now looking to drive When asked about advice he has for other Backstage adopters, Scott talked about the DevOps team's comms strategy and ensuring end-user developers had pathways to provide feedback. In addition to the focus on templates for the new services, having both a dedicated Slack channel and open-door communication with the DevOps team helped reduce onboarding friction to the DevOps Kubernetes platform. -Outside of that, he believes the approach to documentation is paramount. "Write your documentation from the perspective of, 'Hey, you have this task assigned to you to write a template, here are the exact steps you have to follow.'"" Scott said. "Think of it as a recipe. Removing your familiarity bias will help make this tool more useful for less familiar teams."" +Outside of that, he believes the approach to documentation is paramount. "Write your documentation from the perspective of, 'Hey, you have this task assigned to you to write a template, here are the exact steps you have to follow'," Scott said. "Think of it as a recipe. Removing your familiarity bias will help make this tool more useful for less familiar teams." Finally, don't be afraid to step outside the core use cases when it comes to the Backstage framework and finding solutions to meet your needs. -"Backstage gives you a lot of really easy to use features right out of the box. And one of the great things about open source is you can look at the code and see exactly what the behavior is and work within that behavior. But we've also developed our own processes for custom tasks because — at the end of the day — our platform has some very custom aspects to it."" Scott said. +"Backstage gives you a lot of really easy to use features right out of the box. And one of the great things about open source is you can look at the code and see exactly what the behavior is and work within that behavior. But we've also developed our own processes for custom tasks because — at the end of the day — our platform has some very custom aspects to it," Scott said. Interested in more stories from Backstage adopters? Check out these recent posts from [Stash](https://backstage.io/blog/2023/07/08/stash-adopter-post) and [Expedia Group](https://backstage.io/blog/2023/08/17/expedia-proof-of-value-metrics-2). -Want to learn more about Backstage? Join our weekly [Office Hours](https://info.backstage.spotify.com/office-hours) and bring your burning questions. +Want to learn more about Backstage? Join our weekly [Office Hours](https://spoti.fi/backstageofficehours) and bring your burning questions. diff --git a/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx b/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx index 0f8bdd21cf..d4b66c6a70 100644 --- a/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx +++ b/microsite/blog/2025-12-30-backstage-wrapped-2025.mdx @@ -43,7 +43,7 @@ Couldn't spot your name in the videos? Here's one more chance — every contribu ## Mature for a five-year-old -![Backstage community stats: 3.4k+ adopters, 1,8k contributors, 15k Discord members, 7k project forks, 68k total contributions, 250+ open source plugins, 31k stars](assets/2025-12-30/wrapped2025-stats.png) +![Backstage community stats: 3.4k+ adopters, 1.8k contributors, 15k Discord members, 7k project forks, 68k total contributions, 250+ open source plugins, 31k stars](assets/2025-12-30/wrapped2025-stats.png) _The growing Backstage ecosystem, by the numbers_ Earlier this year, the Backstage project [celebrated its 5th birthday][bday] on the main stage at [KubeCon + CloudNativeCon in London][kcon-eu]. diff --git a/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx b/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx new file mode 100644 index 0000000000..67941fa436 --- /dev/null +++ b/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx @@ -0,0 +1,48 @@ +--- +title: 'Get a jump on ContribFest with the new web app' +author: Elaine de Mattos Silva Bezerra, DB Systel GmbH, Heikki Hellgren, OP Financial Group & André Wanlin, Spotify +--- + +![Get a jump on ContribFest with the new web app](assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png) + +Become a Contrib Champ and join us at ContribFest, where commits become legendary! + +We are once again hosting ContribFest at KubeCon + CloudNativeCon. This time around, it's taking place in Amsterdam on March 26, 2026, at 13:45 CET — make sure to [add it to your schedule](https://kccnceu2026.sched.com/event/2EF7v/contribfest-supercharge-your-open-source-impact-backstage-contribfest-live-andre-wanlin-emma-indal-spotify-heikki-hellgren-op-financial-group-elaine-bezerra-db-systel-gmbh?iframe=no). Learn more about what to expect below and get started now by exploring the new [ContribFest web app](https://contribfest.backstage.io/). + +{/* truncate */} + +## Introducing the ContribFest web app + +We're excited to announce the new ContribFest web app: [https://contribfest.backstage.io/](https://contribfest.backstage.io/). The app simplifies local setup and helps you quickly find good issues to work on from the curated list pre-selected by your ContribFest co-hosts. + +You'll see that the app is broken down into five sections: + +- [Welcome](https://contribfest.backstage.io/): This is where you'll find links to all the things, including the session's slide deck, assignment sheet, the Backstage and Community Plugins repositories, and their respective contribution guides. +- [Getting Started](https://contribfest.backstage.io/getting-started/): Whether you are new to Backstage or an old hat, use this handy checklist to help you get your local environment set up for contributing, including all the commands. (Make sure you check all the boxes, you never know what might happen! 😉) +- [Curated Issues](https://contribfest.backstage.io/issues/): This is what you come to the session for: finding an issue that speaks to you and contributing towards it. This section has a list of issues that we've curated — and filters, so you can slice and dice the list to find the perfect issue to work on. +- [Contrib Champs](https://contribfest.backstage.io/contrib-champs/): We've hosted three other ContribFests in the past — this is where you'll find merged PRs from those sessions, a place to celebrate contributions. Make sure to tag your PRs with “ContribFest”, and maybe your name will show up here one day, too! 🏆 +- [Hall of Hosts](https://contribfest.backstage.io/hall-of-hosts/): ContribFest would not take place without the various community members who have stepped up to help co-host the sessions. This is where you'll see an honor roll of past co-hosts. 🙏 + +## About those Contrib Champs + +The goals of the Backstage ContribFest sessions are many — foster community, work with experts, etc. — but it's pretty obvious that contributions are the most important. It's in the name after all. Here are a few past contributions that we wanted to share to give you an idea of what that looks like: + +- [#27694](https://github.com/backstage/backstage/pull/27694) by [hyb175](https://github.com/hyb175) — Add Pagination to Tech Docs Table: for those with lots of entities with TechDocs, this is a massive performance improvement. +- [#29470](https://github.com/backstage/backstage/pull/29470) by [ioboi](https://github.com/ioboi) — Openshift Auth provider: this allows those using OpenShift to use it to sign into their Backstage instance. +- [#31770](https://github.com/backstage/backstage/pull/31770) by [theZMC](https://github.com/theZMC) — Render HTML in GitHub-flavored Markdown: with this change in place, HTML will now render correctly in the MarkdownContent component when you are using the GitHub-flavored Markdown mode. + +Check out the [Contrib Champs page](https://contribfest.backstage.io/contrib-champs/) to see the full list! + +## Using Dev Containers + +Along with the new ContribFest web app, we are also looking to use Dev Containers this time around to help streamline the session for those who'd like to use that option to get started. On the [Getting Started page](https://contribfest.backstage.io/getting-started/), pick the Dev Containers radio button and then follow the checklist. To give you a quick preview, you'll need to have the following installed: + +- Git, you'll need this to be able to pull down the code +- Docker Desktop (or Docker Engine on Linux) +- VS Code with the Dev Containers extension or IntelliJ IDEA Ultimate + +Check out our [Dev Containers tutorial](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/devcontainer.md) for a deeper dive into the subject. + +## Amsterdam, here we come! + +On behalf of the Backstage ContribFest co-host team, thank you for following along. We're looking forward to meeting you in Amsterdam and working together on your contributions. Please be sure to introduce yourself! diff --git a/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png b/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png new file mode 100644 index 0000000000..21e726aeb7 Binary files /dev/null and b/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png differ diff --git a/microsite/data/plugins/3scale.yaml b/microsite/data/plugins/3scale.yaml index 1b27e1ee84..87ab503b8a 100644 --- a/microsite/data/plugins/3scale.yaml +++ b/microsite/data/plugins/3scale.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/3scale.svg npmPackageName: '@backstage-community/plugin-3scale-backend' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/adr.yaml b/microsite/data/plugins/adr.yaml index 003ae22e7b..35b93fcbdc 100644 --- a/microsite/data/plugins/adr.yaml +++ b/microsite/data/plugins/adr.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/adr-logo.png npmPackageName: '@backstage-community/plugin-adr' addedDate: '2022-04-13' +status: active diff --git a/microsite/data/plugins/ai-assistant.yaml b/microsite/data/plugins/ai-assistant.yaml index 6ac4b6cd9e..55b4b56e68 100644 --- a/microsite/data/plugins/ai-assistant.yaml +++ b/microsite/data/plugins/ai-assistant.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/ai-assistant-rag-ai/ iconUrl: https://roadie.io/images/logos/roadie-racks-ai.png npmPackageName: '@roadiehq/rag-ai' addedDate: '2024-03-12' +status: active diff --git a/microsite/data/plugins/airbrake.yaml b/microsite/data/plugins/airbrake.yaml index 59910df629..73ce4d9874 100644 --- a/microsite/data/plugins/airbrake.yaml +++ b/microsite/data/plugins/airbrake.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://wp-assets.airbrake.io/wp-content/uploads/2020/10/05222904/Square-white-A-on-Orange.png npmPackageName: '@backstage-community/plugin-airbrake' addedDate: '2022-01-10' +status: active diff --git a/microsite/data/plugins/allure.yaml b/microsite/data/plugins/allure.yaml index cbcd0ec435..6302ffb3c7 100644 --- a/microsite/data/plugins/allure.yaml +++ b/microsite/data/plugins/allure.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars.githubusercontent.com/u/5879127 npmPackageName: '@backstage-community/plugin-allure' addedDate: '2021-09-02' +status: active diff --git a/microsite/data/plugins/amplication.yaml b/microsite/data/plugins/amplication.yaml index a1b5f09c0e..15fb08e83f 100644 --- a/microsite/data/plugins/amplication.yaml +++ b/microsite/data/plugins/amplication.yaml @@ -8,3 +8,4 @@ documentation: https://docs.amplication.com/day-zero/integrate-with-backstage iconUrl: /img/amplication_logo.png npmPackageName: '@backstage-community/plugin-scaffolder-backend-module-amplication' addedDate: '2025-03-07' +status: active diff --git a/microsite/data/plugins/analytics-module-ga.yaml b/microsite/data/plugins/analytics-module-ga.yaml index 537a360c8c..58806e127e 100644 --- a/microsite/data/plugins/analytics-module-ga.yaml +++ b/microsite/data/plugins/analytics-module-ga.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/ga-icon.png npmPackageName: '@backstage-community/plugin-analytics-module-ga4' addedDate: '2021-10-07' +status: active diff --git a/microsite/data/plugins/analytics-module-generic.yaml b/microsite/data/plugins/analytics-module-generic.yaml index 6772ee6d2d..b34f45425e 100644 --- a/microsite/data/plugins/analytics-module-generic.yaml +++ b/microsite/data/plugins/analytics-module-generic.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/pfeifferj/backstage-plugin-analytics-generic/b iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@pfeifferj/backstage-plugin-analytics-generic' addedDate: '2024-08-23' +status: active diff --git a/microsite/data/plugins/analytics-module-matomo.yaml b/microsite/data/plugins/analytics-module-matomo.yaml index 88ce08c868..b5dca4e662 100644 --- a/microsite/data/plugins/analytics-module-matomo.yaml +++ b/microsite/data/plugins/analytics-module-matomo.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/matomo.png npmPackageName: '@backstage-community/plugin-analytics-module-matomo' addedDate: '2023-10-17' +status: active diff --git a/microsite/data/plugins/analytics-module-qm.yaml b/microsite/data/plugins/analytics-module-qm.yaml index df2ae3e935..00c07c7589 100644 --- a/microsite/data/plugins/analytics-module-qm.yaml +++ b/microsite/data/plugins/analytics-module-qm.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/quantummetric/analytics-module-qm/blob/main/RE iconUrl: /img/qm-icon.png npmPackageName: '@quantum-metric/plugin-analytics-module-qm' addedDate: '2024-03-15' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/announcements.yaml b/microsite/data/plugins/announcements.yaml index f24965f10a..d7442ff740 100644 --- a/microsite/data/plugins/announcements.yaml +++ b/microsite/data/plugins/announcements.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/plugin-announcements-logo.png npmPackageName: '@backstage-community/plugin-announcements' addedDate: '2022-11-09' +status: active diff --git a/microsite/data/plugins/api-docs.yaml b/microsite/data/plugins/api-docs.yaml index 645e32c54b..c60703a20b 100644 --- a/microsite/data/plugins/api-docs.yaml +++ b/microsite/data/plugins/api-docs.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/api-do iconUrl: https://raw.githubusercontent.com/vscode-icons/vscode-icons/master/icons/file_type_swagger.svg npmPackageName: '@backstage/plugin-api-docs' addedDate: '2020-11-19' +status: active diff --git a/microsite/data/plugins/api-linter.yaml b/microsite/data/plugins/api-linter.yaml index 837efdffc0..25d80c7928 100644 --- a/microsite/data/plugins/api-linter.yaml +++ b/microsite/data/plugins/api-linter.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/zalando/backstage-plugin-api-linter iconUrl: https://raw.githubusercontent.com/zalando/zally/main/logo.png npmPackageName: backstage-plugin-api-linter addedDate: '2022-07-22' +status: active diff --git a/microsite/data/plugins/api-sdk-generator.yaml b/microsite/data/plugins/api-sdk-generator.yaml index 5553089807..19bc165fa8 100644 --- a/microsite/data/plugins/api-sdk-generator.yaml +++ b/microsite/data/plugins/api-sdk-generator.yaml @@ -6,5 +6,7 @@ category: Development description: Generate SDKs for your REST API to accelerate integration documentation: https://github.com/konfig-dev/backstage-plugin-konfig/tree/main/plugins/backstage-plugin-konfig iconUrl: https://raw.githubusercontent.com/konfig-dev/backstage-plugin-konfig/main/plugins/backstage-plugin-konfig/docs/logo.png -npmPackageName: 'backstage-plugin-konfig' +npmPackageName: backstage-plugin-konfig addedDate: '2023-08-07' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/api-spectral-linter.yaml b/microsite/data/plugins/api-spectral-linter.yaml index 5f1682c71b..ae94220793 100644 --- a/microsite/data/plugins/api-spectral-linter.yaml +++ b/microsite/data/plugins/api-spectral-linter.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/api-docs-spectral-linter/docs/pluginIcon.png npmPackageName: '@dweber019/backstage-plugin-api-docs-spectral-linter' addedDate: '2023-03-27' +status: active diff --git a/microsite/data/plugins/api-wsdl.yaml b/microsite/data/plugins/api-wsdl.yaml index 219227c509..ab5c99bc84 100644 --- a/microsite/data/plugins/api-wsdl.yaml +++ b/microsite/data/plugins/api-wsdl.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/api-docs-module-wsdl/docs/logo.png npmPackageName: '@dweber019/backstage-plugin-api-docs-module-wsdl' addedDate: '2023-12-22' +status: active diff --git a/microsite/data/plugins/apicurio-registry.yaml b/microsite/data/plugins/apicurio-registry.yaml index 3750fbaa69..ae89d7fd56 100644 --- a/microsite/data/plugins/apicurio-registry.yaml +++ b/microsite/data/plugins/apicurio-registry.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/simonnepomuk/backstage-plugin-apicurio-registr iconUrl: https://avatars.githubusercontent.com/u/28107283?s=200&v=4 npmPackageName: '@simonnepomuk/backstage-plugin-apicurio-registry' addedDate: '2025-08-31' +status: active diff --git a/microsite/data/plugins/apiiro.yaml b/microsite/data/plugins/apiiro.yaml index ce3db54565..dffaaceabf 100644 --- a/microsite/data/plugins/apiiro.yaml +++ b/microsite/data/plugins/apiiro.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://avatars.githubusercontent.com/u/48519090 npmPackageName: '@backstage-community/plugin-apiiro' addedDate: '2026-01-27' +status: active diff --git a/microsite/data/plugins/apollo-explorer.yaml b/microsite/data/plugins/apollo-explorer.yaml index 83d121e055..60eb09ce2a 100644 --- a/microsite/data/plugins/apollo-explorer.yaml +++ b/microsite/data/plugins/apollo-explorer.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/apollo-explorer.png npmPackageName: '@backstage-community/plugin-apollo-explorer' addedDate: '2022-07-20' +status: active diff --git a/microsite/data/plugins/argo-cd.yaml b/microsite/data/plugins/argo-cd.yaml index f183ad9ffd..bd7802403c 100644 --- a/microsite/data/plugins/argo-cd.yaml +++ b/microsite/data/plugins/argo-cd.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/argo-cd/?utm_source=backstage iconUrl: https://roadie.io/images/logos/argo.png npmPackageName: '@roadiehq/backstage-plugin-argo-cd' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/artifactory.yaml b/microsite/data/plugins/artifactory.yaml index 119e09eb8e..5758590075 100644 --- a/microsite/data/plugins/artifactory.yaml +++ b/microsite/data/plugins/artifactory.yaml @@ -10,3 +10,5 @@ documentation: https://github.com/StageCentral/backstage-artifactory-plugin/tree iconUrl: https://raw.githubusercontent.com/StageCentral/backstage-artifactory-plugin/main/logo/logo.png npmPackageName: '@stagecentral/plugin-artifactory' addedDate: '2023-08-23' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/aws-amazon-ecs.yaml b/microsite/data/plugins/aws-amazon-ecs.yaml index e16028d802..55a4791f3c 100644 --- a/microsite/data/plugins/aws-amazon-ecs.yaml +++ b/microsite/data/plugins/aws-amazon-ecs.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/pl iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/amazon-ecs-logo.png npmPackageName: '@aws/amazon-ecs-plugin-for-backstage' addedDate: '2024-04-22' +status: active diff --git a/microsite/data/plugins/aws-cloudformation.yaml b/microsite/data/plugins/aws-cloudformation.yaml index 6ee36e82b0..f82c4d64c4 100644 --- a/microsite/data/plugins/aws-cloudformation.yaml +++ b/microsite/data/plugins/aws-cloudformation.yaml @@ -6,5 +6,7 @@ category: Infrastructure description: Load Backstage entities from AWS CloudFormation stacks documentation: https://github.com/purple-technology/backstage-aws-cloudformation-plugin#readme iconUrl: https://raw.githubusercontent.com/purple-technology/backstage-aws-cloudformation-plugin/master/docs/cloudformation-logo.png -npmPackageName: 'backstage-aws-cloudformation-plugin' +npmPackageName: backstage-aws-cloudformation-plugin addedDate: '2021-08-30' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/aws-codebuild.yaml b/microsite/data/plugins/aws-codebuild.yaml index 504730402f..687362299d 100644 --- a/microsite/data/plugins/aws-codebuild.yaml +++ b/microsite/data/plugins/aws-codebuild.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/pl iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/aws-codebuild-logo.png npmPackageName: '@aws/aws-codebuild-plugin-for-backstage' addedDate: '2024-04-22' +status: active diff --git a/microsite/data/plugins/aws-codepipeline.yaml b/microsite/data/plugins/aws-codepipeline.yaml index a2eb8de230..8475b97082 100644 --- a/microsite/data/plugins/aws-codepipeline.yaml +++ b/microsite/data/plugins/aws-codepipeline.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/awslabs/backstage-plugins-for-aws/tree/main/pl iconUrl: https://raw.githubusercontent.com/awslabs/backstage-plugins-for-aws/main/docs/images/logos/aws-codepipeline-logo.png npmPackageName: '@aws/aws-codepipeline-plugin-for-backstage' addedDate: '2024-04-22' +status: active diff --git a/microsite/data/plugins/aws-lambda.yaml b/microsite/data/plugins/aws-lambda.yaml index 80cebd06a8..33d4e795b3 100644 --- a/microsite/data/plugins/aws-lambda.yaml +++ b/microsite/data/plugins/aws-lambda.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/aws-lambda/?utm_source=backst iconUrl: https://roadie.io/images/logos/lambda.png npmPackageName: '@roadiehq/backstage-plugin-aws-lambda' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/azure-pipelines.yaml b/microsite/data/plugins/azure-pipelines.yaml index f9536eb192..1e39ce2132 100644 --- a/microsite/data/plugins/azure-pipelines.yaml +++ b/microsite/data/plugins/azure-pipelines.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/azure-pipelines.svg npmPackageName: '@backstage-community/plugin-azure-devops' addedDate: '2021-12-22' +status: active diff --git a/microsite/data/plugins/azure-resources.yaml b/microsite/data/plugins/azure-resources.yaml index 1c5264e3f7..3a76117066 100644 --- a/microsite/data/plugins/azure-resources.yaml +++ b/microsite/data/plugins/azure-resources.yaml @@ -5,6 +5,8 @@ authorUrl: https://vipps.no category: Infrastructure description: A plugin showing Azure resource groups and security recommendations in relation to an entity in the catalog. documentation: https://github.com/vippsas/backstage-azure-resource-frontend -iconUrl: '/img/logo-gradient-on-dark.svg' +iconUrl: /img/logo-gradient-on-dark.svg npmPackageName: '@vippsno/plugin-azure-resources' addedDate: '2022-09-05' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/azure-sites.yaml b/microsite/data/plugins/azure-sites.yaml index 8d473bdb4f..d0103128d8 100644 --- a/microsite/data/plugins/azure-sites.yaml +++ b/microsite/data/plugins/azure-sites.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@backstage-community/plugin-azure-sites' addedDate: '2022-10-18' +status: active diff --git a/microsite/data/plugins/azure-spring-apps.yaml b/microsite/data/plugins/azure-spring-apps.yaml index e0b77a42c8..c010e71206 100644 --- a/microsite/data/plugins/azure-spring-apps.yaml +++ b/microsite/data/plugins/azure-spring-apps.yaml @@ -1,5 +1,5 @@ --- -title: 'Azure Spring Apps' +title: Azure Spring Apps author: Enfuse authorUrl: https://enfuse.io/ category: Discovery @@ -8,3 +8,5 @@ documentation: https://github.com/enfuse/asae-backstage-plugin/blob/main/README. iconUrl: /img/enfuse.png npmPackageName: '@enfuse/plugin-azure-spring-apps' addedDate: '2022-11-21' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/azure-storage.yaml b/microsite/data/plugins/azure-storage.yaml index e8fbc0fa10..f7964ad0c9 100644 --- a/microsite/data/plugins/azure-storage.yaml +++ b/microsite/data/plugins/azure-storage.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/azure-storage-folder.png npmPackageName: '@backstage-community/plugin-azure-storage-explorer' addedDate: '2023-12-01' +status: active diff --git a/microsite/data/plugins/backchat.yaml b/microsite/data/plugins/backchat.yaml index dd09938307..01b37369fe 100644 --- a/microsite/data/plugins/backchat.yaml +++ b/microsite/data/plugins/backchat.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/benwilcock/backstage-plugin-backchat iconUrl: /img/backchat-logo.png npmPackageName: '@benbravo73/backstage-plugin-backchat' addedDate: '2024-01-12' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/backstage-kubernetes.yaml b/microsite/data/plugins/backstage-kubernetes.yaml index f9514880b0..62ba4e0368 100644 --- a/microsite/data/plugins/backstage-kubernetes.yaml +++ b/microsite/data/plugins/backstage-kubernetes.yaml @@ -9,3 +9,4 @@ iconUrl: /img/backstage-k8s.svg npmPackageName: '@backstage/plugin-kubernetes' order: 4 addedDate: '2021-03-29' +status: active diff --git a/microsite/data/plugins/backstage-search-platform.yaml b/microsite/data/plugins/backstage-search-platform.yaml index fa069d8875..af11209211 100644 --- a/microsite/data/plugins/backstage-search-platform.yaml +++ b/microsite/data/plugins/backstage-search-platform.yaml @@ -9,3 +9,4 @@ iconUrl: /img/backstage-search-platform.svg npmPackageName: '@backstage/plugin-search' order: 5 addedDate: '2022-09-07' +status: active diff --git a/microsite/data/plugins/backstage-software-catalog.yaml b/microsite/data/plugins/backstage-software-catalog.yaml index 3d47acabc7..3433ac4e8e 100644 --- a/microsite/data/plugins/backstage-software-catalog.yaml +++ b/microsite/data/plugins/backstage-software-catalog.yaml @@ -9,3 +9,4 @@ iconUrl: /img/backstage-software-catalog.svg npmPackageName: '@backstage/plugin-catalog' order: 1 addedDate: '2021-06-17' +status: active diff --git a/microsite/data/plugins/backstage-software-templates.yaml b/microsite/data/plugins/backstage-software-templates.yaml index 916f754a2a..6e29deedd6 100644 --- a/microsite/data/plugins/backstage-software-templates.yaml +++ b/microsite/data/plugins/backstage-software-templates.yaml @@ -9,3 +9,4 @@ iconUrl: /img/backstage-software-templates.svg npmPackageName: '@backstage/plugin-scaffolder' order: 2 addedDate: '2021-03-29' +status: active diff --git a/microsite/data/plugins/backstage-techdocs.yaml b/microsite/data/plugins/backstage-techdocs.yaml index ea462a6c94..adbb5608c9 100644 --- a/microsite/data/plugins/backstage-techdocs.yaml +++ b/microsite/data/plugins/backstage-techdocs.yaml @@ -9,3 +9,4 @@ iconUrl: /img/backstage-techdocs.svg npmPackageName: '@backstage/plugin-techdocs' order: 3 addedDate: '2021-03-29' +status: active diff --git a/microsite/data/plugins/badges.yaml b/microsite/data/plugins/badges.yaml index 9452054810..80d3b633f7 100644 --- a/microsite/data/plugins/badges.yaml +++ b/microsite/data/plugins/badges.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/badges.svg npmPackageName: '@backstage-community/plugin-badges' addedDate: '2021-09-29' +status: active diff --git a/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml b/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml new file mode 100644 index 0000000000..6b8227c629 --- /dev/null +++ b/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml @@ -0,0 +1,11 @@ +--- +title: Template Builder +author: Balaji Sivasubramanian +authorUrl: https://github.com/balajisiva +category: Tooling +description: Visual editor for creating and managing Backstage scaffolder templates with live YAML preview, flow visualization, and GitHub integration +documentation: https://github.com/balajisiva/backstage-template-builder#readme +iconUrl: https://raw.githubusercontent.com/balajisiva/backstage-template-builder/main/plugins/backstage-template-builder/icon.svg +npmPackageName: '@balajisiva/backstage-plugin-template-builder' +addedDate: '2026-02-16' +status: active diff --git a/microsite/data/plugins/bazaar.yaml b/microsite/data/plugins/bazaar.yaml index d14aaaecd8..ba64c622f4 100644 --- a/microsite/data/plugins/bazaar.yaml +++ b/microsite/data/plugins/bazaar.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/bazaar.svg npmPackageName: '@backstage-community/plugin-bazaar' addedDate: '2022-01-11' +status: active diff --git a/microsite/data/plugins/betterscan.yaml b/microsite/data/plugins/betterscan.yaml index 852ae0081c..edc3caeedd 100644 --- a/microsite/data/plugins/betterscan.yaml +++ b/microsite/data/plugins/betterscan.yaml @@ -8,3 +8,5 @@ documentation: https://www.npmjs.com/package/@marcinguy/backstage-plugin-betters iconUrl: https://uploads-ssl.webflow.com/6339e3b81867539b5fe2498d/633a1643dcb06d3029867161_g4.svg npmPackageName: '@marcinguy/backstage-plugin-betterscan' addedDate: '2022-12-08' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/bitrise.yaml b/microsite/data/plugins/bitrise.yaml index 31f50a007c..57fc360580 100644 --- a/microsite/data/plugins/bitrise.yaml +++ b/microsite/data/plugins/bitrise.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://avatars.githubusercontent.com/u/7174390?s=400&v=4 npmPackageName: '@backstage-community/plugin-bitrise' addedDate: '2021-03-01' +status: active diff --git a/microsite/data/plugins/blackduck.yaml b/microsite/data/plugins/blackduck.yaml index 73b319b0ce..db81068d85 100644 --- a/microsite/data/plugins/blackduck.yaml +++ b/microsite/data/plugins/blackduck.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars.githubusercontent.com/u/431461?s=200&v=4 npmPackageName: '@backstage-community/plugin-blackduck' addedDate: '2022-12-03' +status: active diff --git a/microsite/data/plugins/blockchain-radar.yaml b/microsite/data/plugins/blockchain-radar.yaml index e122d376ce..2af951d3e6 100644 --- a/microsite/data/plugins/blockchain-radar.yaml +++ b/microsite/data/plugins/blockchain-radar.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/aurora-is-near/backstage-plugin-blockchainrada iconUrl: https://raw.githubusercontent.com/aurora-is-near/backstage-plugin-blockchainradar/main/docs/imgs/signs/sign-green.svg npmPackageName: '@aurora-is-near/backstage-plugin-blockchainradar-backend' addedDate: '2023-08-31' +status: active diff --git a/microsite/data/plugins/bugsnag.yaml b/microsite/data/plugins/bugsnag.yaml index dc593ce5e8..59ccb8f6dc 100644 --- a/microsite/data/plugins/bugsnag.yaml +++ b/microsite/data/plugins/bugsnag.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/bugsnag/?utm_source=backstage iconUrl: https://roadie.io/images/logos/bugsnag.png npmPackageName: '@roadiehq/backstage-plugin-bugsnag' addedDate: '2021-09-24' +status: active diff --git a/microsite/data/plugins/buildkite.yaml b/microsite/data/plugins/buildkite.yaml index 48754f6ae6..726d886de4 100644 --- a/microsite/data/plugins/buildkite.yaml +++ b/microsite/data/plugins/buildkite.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/buildkite/backstage-plugin iconUrl: /img/buildkite.svg npmPackageName: '@buildkite/backstage-plugin-buildkite' addedDate: '2025-04-12' +status: active diff --git a/microsite/data/plugins/bulletin-board.yaml b/microsite/data/plugins/bulletin-board.yaml index 67791f6f84..1c56932e2c 100644 --- a/microsite/data/plugins/bulletin-board.yaml +++ b/microsite/data/plugins/bulletin-board.yaml @@ -6,5 +6,7 @@ category: Discovery description: Share interesting ideas, news and links with your teammates within Backstage. documentation: https://github.com/v-ngu/backstage-plugin-bulletin-board iconUrl: /img/bulletin-board.png -npmPackageName: 'backstage-plugin-bulletin-board' +npmPackageName: backstage-plugin-bulletin-board addedDate: '2023-04-01' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/catalog-backend-module-gcp.yaml b/microsite/data/plugins/catalog-backend-module-gcp.yaml index 6fde9134a9..eda1729812 100644 --- a/microsite/data/plugins/catalog-backend-module-gcp.yaml +++ b/microsite/data/plugins/catalog-backend-module-gcp.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backtostage/backstage-plugins/blob/main/plugin iconUrl: https://avatars1.githubusercontent.com/u/2810941?s=280&v=4 npmPackageName: '@backtostage/plugin-catalog-backend-module-gcp' addedDate: '2024-08-01' +status: active diff --git a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml index e68a423633..91a286ca29 100644 --- a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml +++ b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/catalo iconUrl: /img/puppet.png npmPackageName: '@backstage/plugin-catalog-backend-module-puppetdb' addedDate: '2023-02-06' +status: active diff --git a/microsite/data/plugins/catalog-graph.yaml b/microsite/data/plugins/catalog-graph.yaml index 46ee7b2f17..b41bb1d920 100644 --- a/microsite/data/plugins/catalog-graph.yaml +++ b/microsite/data/plugins/catalog-graph.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/catalo iconUrl: /img/catalog-graph.svg npmPackageName: '@backstage/plugin-catalog-graph' addedDate: '2021-09-15' +status: active diff --git a/microsite/data/plugins/catalog-webhook.yaml b/microsite/data/plugins/catalog-webhook.yaml index 5c72cdd605..1fc2a87979 100644 --- a/microsite/data/plugins/catalog-webhook.yaml +++ b/microsite/data/plugins/catalog-webhook.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/ForgedApps/backstage-catalog-webhook-plugin/bl iconUrl: /img/forgedapps.jpg npmPackageName: '@forgedapps/backstage-catalog-webhook-plugin' addedDate: '2024-10-28' +status: active diff --git a/microsite/data/plugins/chatgpt-playground.yaml b/microsite/data/plugins/chatgpt-playground.yaml index 79146adc2c..9c39c5f0d9 100644 --- a/microsite/data/plugins/chatgpt-playground.yaml +++ b/microsite/data/plugins/chatgpt-playground.yaml @@ -1,5 +1,5 @@ --- -title: 'ChatGPT Playground' +title: ChatGPT Playground author: Enfuse authorUrl: https://enfuse.io/ category: Development @@ -8,3 +8,5 @@ documentation: https://github.com/enfuse/backstage-chatgpt-plugin/blob/main/READ iconUrl: /img/enfuse.png npmPackageName: '@enfuse/chatgpt-plugin-frontend' addedDate: '2023-05-23' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/cicd-statistics.yaml b/microsite/data/plugins/cicd-statistics.yaml index 61d083b19a..1e274e7805 100644 --- a/microsite/data/plugins/cicd-statistics.yaml +++ b/microsite/data/plugins/cicd-statistics.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/cicd-statistics.svg npmPackageName: '@backstage-community/plugin-cicd-statistics' addedDate: '2022-01-13' +status: active diff --git a/microsite/data/plugins/circleci.yaml b/microsite/data/plugins/circleci.yaml index ba68fe838d..ec11b44b2a 100644 --- a/microsite/data/plugins/circleci.yaml +++ b/microsite/data/plugins/circleci.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/CircleCI-Public/backstage-plugin/tree/main/plu iconUrl: /img/circleci.png npmPackageName: '@circleci/backstage-plugin' addedDate: '2021-04-28' +status: active diff --git a/microsite/data/plugins/cloud-build.yaml b/microsite/data/plugins/cloud-build.yaml index e3e23b429f..80d9899096 100644 --- a/microsite/data/plugins/cloud-build.yaml +++ b/microsite/data/plugins/cloud-build.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars2.githubusercontent.com/u/38220399?s=400&v=4 npmPackageName: '@backstage-community/plugin-cloudbuild' addedDate: '2021-01-20' +status: active diff --git a/microsite/data/plugins/cloud-carbon-footprint.yaml b/microsite/data/plugins/cloud-carbon-footprint.yaml index 6a318eb55d..596dbb08a2 100644 --- a/microsite/data/plugins/cloud-carbon-footprint.yaml +++ b/microsite/data/plugins/cloud-carbon-footprint.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/cloud-carbon-footprint/ccf-backstage-plugin/bl iconUrl: https://www.cloudcarbonfootprint.org/img/logo.png npmPackageName: '@cloud-carbon-footprint/backstage-plugin-frontend' addedDate: '2022-05-03' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/cloudify.yaml b/microsite/data/plugins/cloudify.yaml index 93b7a26423..3909a0f398 100644 --- a/microsite/data/plugins/cloudify.yaml +++ b/microsite/data/plugins/cloudify.yaml @@ -6,5 +6,7 @@ category: Orchestration description: Cloudify provides a remote execution and environment management backend for Kubernetes, Terraform, Ansible, etc. documentation: https://github.com/cloudify-cosmo/backstage-cloudify-plugin#readme iconUrl: https://avatars.githubusercontent.com/u/6260555?s=200&v=4 -npmPackageName: 'plugin-cloudify' +npmPackageName: plugin-cloudify addedDate: '2022-05-31' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/cloudsmith.yaml b/microsite/data/plugins/cloudsmith.yaml index b9d8c0ffbf..b057c71b97 100644 --- a/microsite/data/plugins/cloudsmith.yaml +++ b/microsite/data/plugins/cloudsmith.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/pl iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg npmPackageName: '@roadiehq/backstage-plugin-cloudsmith' addedDate: '2022-11-18' +status: active diff --git a/microsite/data/plugins/codacy-repo-adder.yaml b/microsite/data/plugins/codacy-repo-adder.yaml index a479a45ecf..d0c2380cbf 100644 --- a/microsite/data/plugins/codacy-repo-adder.yaml +++ b/microsite/data/plugins/codacy-repo-adder.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/codacy/backstage-plugin/blob/main/README.md iconUrl: /img/codacy-icon.svg npmPackageName: '@codacy/backstage-plugin' addedDate: '2024-04-19' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/coder.yaml b/microsite/data/plugins/coder.yaml index 71d32b5ce4..2345fc8bcb 100644 --- a/microsite/data/plugins/coder.yaml +++ b/microsite/data/plugins/coder.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/coder/backstage-plugins/blob/main/plugins/back iconUrl: /img/coder.png npmPackageName: '@coder/backstage-plugin-coder' addedDate: '2024-03-15' +status: active diff --git a/microsite/data/plugins/codescene.yaml b/microsite/data/plugins/codescene.yaml index b69124aecc..6fb47465a6 100644 --- a/microsite/data/plugins/codescene.yaml +++ b/microsite/data/plugins/codescene.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/codescene_logo.svg npmPackageName: '@backstage-community/plugin-codescene' addedDate: '2022-04-12' +status: active diff --git a/microsite/data/plugins/conviso-platform.yaml b/microsite/data/plugins/conviso-platform.yaml index 7017f74a3f..2380e39ff2 100644 --- a/microsite/data/plugins/conviso-platform.yaml +++ b/microsite/data/plugins/conviso-platform.yaml @@ -6,5 +6,6 @@ category: Security description: Import your Backstage catalog entities as security assets in Conviso Platform. documentation: https://github.com/convisoappsec/backstage-plugin-conviso/blob/main/README.md iconUrl: https://raw.githubusercontent.com/convisoappsec/backstage-plugin-conviso/main/assets/convisoappsec_logo.png -npmPackageName: 'backstage-plugin-conviso' +npmPackageName: '@conviso/backstage-plugin-conviso' addedDate: '2025-11-14' +status: active diff --git a/microsite/data/plugins/cortex.yaml b/microsite/data/plugins/cortex.yaml index 09c904b6f3..de3fe52f61 100644 --- a/microsite/data/plugins/cortex.yaml +++ b/microsite/data/plugins/cortex.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/cortexapps/backstage-plugin iconUrl: /img/cortex.png npmPackageName: '@cortexapps/backstage-plugin' addedDate: '2021-06-03' +status: active diff --git a/microsite/data/plugins/cost-insights.yaml b/microsite/data/plugins/cost-insights.yaml index b43f6b1af1..0b8339170b 100644 --- a/microsite/data/plugins/cost-insights.yaml +++ b/microsite/data/plugins/cost-insights.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/cost-insights.png npmPackageName: '@backstage-community/plugin-cost-insights' addedDate: '2021-04-28' +status: active diff --git a/microsite/data/plugins/crossplane.yaml b/microsite/data/plugins/crossplane.yaml index 0fe66ae43c..28e3101cb3 100644 --- a/microsite/data/plugins/crossplane.yaml +++ b/microsite/data/plugins/crossplane.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugin iconUrl: https://github.com/cncf/artwork/blob/main/projects/crossplane/icon/color/crossplane-icon-color.png?raw=true npmPackageName: '@terasky/backstage-plugin-crossplane-resources-frontend' addedDate: '2024-12-30' +status: active diff --git a/microsite/data/plugins/cyclops-modules.yaml b/microsite/data/plugins/cyclops-modules.yaml index 2abc9145ff..d4e28e957f 100644 --- a/microsite/data/plugins/cyclops-modules.yaml +++ b/microsite/data/plugins/cyclops-modules.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/cyclops-ui/backstage-plugins#readme iconUrl: /img/cyclops.svg npmPackageName: '@cyclopsui/backstage-plugin-cyclops-modules' addedDate: '2024-01-02' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/daily-weather-update.yaml b/microsite/data/plugins/daily-weather-update.yaml index 4118a4d5d6..3009e4dff1 100644 --- a/microsite/data/plugins/daily-weather-update.yaml +++ b/microsite/data/plugins/daily-weather-update.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Infosys/daily-weather-plugin/tree/main/plugins iconUrl: https://github.com/Infosys/daily-weather-plugin/blob/main/plugins/weather/src/docs/plugin-logo.png?raw=true npmPackageName: '@infosys_ltd/daily-weather-plugin' addedDate: '2025-04-16' +status: active diff --git a/microsite/data/plugins/datacontract.yaml b/microsite/data/plugins/datacontract.yaml index a267730496..faf36bc182 100644 --- a/microsite/data/plugins/datacontract.yaml +++ b/microsite/data/plugins/datacontract.yaml @@ -8,3 +8,4 @@ documentation: https://www.npmjs.com/package/@remunda/backstage-plugin-datacontr iconUrl: https://www.remunda.cz/logos/datacontract.png npmPackageName: '@remunda/backstage-plugin-datacontract' addedDate: '2025-08-19' +status: active diff --git a/microsite/data/plugins/datadog-entity-sync.yaml b/microsite/data/plugins/datadog-entity-sync.yaml index 2f0cd06cd5..23eeda66cf 100644 --- a/microsite/data/plugins/datadog-entity-sync.yaml +++ b/microsite/data/plugins/datadog-entity-sync.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/DataDog/datadog-backstage-plugins/tree/main/pl iconUrl: https://imgix.datadoghq.com/img/about/presskit/logo-v/dd_vertical_purple.png?auto=format&fit=max&w=847&dpr=2 npmPackageName: '@datadog/backstage-plugin-datadog-entity-sync-backend' addedDate: '2025-09-19' +status: active diff --git a/microsite/data/plugins/datadog.yaml b/microsite/data/plugins/datadog.yaml index 0a720698f9..0816a0510d 100644 --- a/microsite/data/plugins/datadog.yaml +++ b/microsite/data/plugins/datadog.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/datadog/?utm_source=backstage iconUrl: https://roadie.io/images/logos/datadog-white-background.png npmPackageName: '@roadiehq/backstage-plugin-datadog' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/daytona.yaml b/microsite/data/plugins/daytona.yaml index 3c009df586..8349165694 100644 --- a/microsite/data/plugins/daytona.yaml +++ b/microsite/data/plugins/daytona.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/daytonaio/backstage-plugins/tree/main/plugins/ iconUrl: /img/daytona.svg npmPackageName: '@daytonaio/backstage-plugin-daytona' addedDate: '2024-11-02' +status: active diff --git a/microsite/data/plugins/dbt.yaml b/microsite/data/plugins/dbt.yaml index d3947fd4a0..5f8788038b 100644 --- a/microsite/data/plugins/dbt.yaml +++ b/microsite/data/plugins/dbt.yaml @@ -5,6 +5,8 @@ authorUrl: https://github.com/IIBenII category: Discovery description: View dbt models and tests documentation. documentation: https://github.com/IIBenII/backstage-plugin-dbt -iconUrl: '/img/logo-gradient-on-dark.svg' +iconUrl: /img/logo-gradient-on-dark.svg npmPackageName: '@iiben_orgii/backstage-plugin-dbt' addedDate: '2023-04-27' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/dependencytrack.yaml b/microsite/data/plugins/dependencytrack.yaml index 1d32550016..6dda1d1969 100644 --- a/microsite/data/plugins/dependencytrack.yaml +++ b/microsite/data/plugins/dependencytrack.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/TRIMM/plugin-dependencytrack/blob/main/README. iconUrl: https://avatars.githubusercontent.com/u/40258585?s=200&v=4 npmPackageName: '@trimm/plugin-dependencytrack' addedDate: '2022-09-06' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/dev-friends-day.yaml b/microsite/data/plugins/dev-friends-day.yaml index 9cbcd5b42f..0b458824e2 100644 --- a/microsite/data/plugins/dev-friends-day.yaml +++ b/microsite/data/plugins/dev-friends-day.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/piatkiewicz/backstage-plugins/tree/main/dev-fr iconUrl: /img/dev-friends-days.jpeg npmPackageName: '@piatkiewicz/backstage-dev-friends-days' addedDate: '2024-02-28' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/dev-quotes.yaml b/microsite/data/plugins/dev-quotes.yaml index ad2ac91e4c..e9d1d37834 100644 --- a/microsite/data/plugins/dev-quotes.yaml +++ b/microsite/data/plugins/dev-quotes.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Parsifal-M/backstage-dev-quotes?tab=readme-ov- iconUrl: /img/dqicon.png npmPackageName: '@parsifal-m/plugin-dev-quotes-homepage' addedDate: '2023-12-13' +status: active diff --git a/microsite/data/plugins/devcontainers.yaml b/microsite/data/plugins/devcontainers.yaml index 427c44b4c6..69dd14b76d 100644 --- a/microsite/data/plugins/devcontainers.yaml +++ b/microsite/data/plugins/devcontainers.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/coder/backstage-plugins/blob/main/plugins/back iconUrl: /img/devcontainers.png npmPackageName: '@coder/backstage-plugin-devcontainers-react' addedDate: '2024-03-15' +status: active diff --git a/microsite/data/plugins/devpod.yaml b/microsite/data/plugins/devpod.yaml index 44f3d54eb5..cb854a440f 100644 --- a/microsite/data/plugins/devpod.yaml +++ b/microsite/data/plugins/devpod.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugin iconUrl: https://raw.githubusercontent.com/loft-sh/devpod/refs/heads/main/docs/static/media/devpod-logo.png npmPackageName: '@terasky/backstage-plugin-devpod' addedDate: '2024-12-30' +status: active diff --git a/microsite/data/plugins/devtools.yaml b/microsite/data/plugins/devtools.yaml index b531195c04..9c6f332f91 100644 --- a/microsite/data/plugins/devtools.yaml +++ b/microsite/data/plugins/devtools.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/devtoo iconUrl: /img/devtools.svg npmPackageName: '@backstage/plugin-devtools' addedDate: '2023-06-17' +status: active diff --git a/microsite/data/plugins/digital.ai-deploy.yaml b/microsite/data/plugins/digital.ai-deploy.yaml index bdff429c3d..585ab4d20c 100644 --- a/microsite/data/plugins/digital.ai-deploy.yaml +++ b/microsite/data/plugins/digital.ai-deploy.yaml @@ -8,3 +8,4 @@ documentation: https://docs.digital.ai/deploy/docs/concept/xl-deploy-backstage-o iconUrl: /img/digital.ai-deploy.svg npmPackageName: '@digital-ai/plugin-dai-deploy' addedDate: '2024-05-06' +status: active diff --git a/microsite/data/plugins/digital.ai-release.yaml b/microsite/data/plugins/digital.ai-release.yaml index 8b46a2000f..af15ce100f 100644 --- a/microsite/data/plugins/digital.ai-release.yaml +++ b/microsite/data/plugins/digital.ai-release.yaml @@ -8,3 +8,4 @@ documentation: https://docs.digital.ai/release/docs/concept/release-backstage-ov iconUrl: /img/digital.ai-release.svg npmPackageName: '@digital-ai/plugin-dai-release' addedDate: '2024-05-28' +status: active diff --git a/microsite/data/plugins/docker-tags.yaml b/microsite/data/plugins/docker-tags.yaml index 88157e1caf..eb8abddfcb 100644 --- a/microsite/data/plugins/docker-tags.yaml +++ b/microsite/data/plugins/docker-tags.yaml @@ -1,10 +1,12 @@ --- title: Docker tags author: Workm8 -authorUrl: 'https://github.com/work-m8' +authorUrl: https://github.com/work-m8 category: Services -description: 'Creates a list of docker tags based on hub.docker.com' -documentation: 'https://github.com/Work-m8/backstage-docker-plugin/blob/main/README.md' -iconUrl: 'https://raw.githubusercontent.com/Work-m8/.github/main/profile/wm_logo.png' +description: Creates a list of docker tags based on hub.docker.com +documentation: https://github.com/Work-m8/backstage-docker-plugin/blob/main/README.md +iconUrl: https://raw.githubusercontent.com/Work-m8/.github/main/profile/wm_logo.png npmPackageName: '@workm8/backstage-docker-tags' addedDate: '2023-10-24' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/dora-metrics.yaml b/microsite/data/plugins/dora-metrics.yaml index 1831c7488f..d612c2bfe9 100644 --- a/microsite/data/plugins/dora-metrics.yaml +++ b/microsite/data/plugins/dora-metrics.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/liatrio/backstage-dora-plugin/tree/main iconUrl: /img/liatrio-icon.png npmPackageName: '@liatrio/backstage-dora-plugin' addedDate: '2024-10-14' +status: active diff --git a/microsite/data/plugins/dora-scorecard.yaml b/microsite/data/plugins/dora-scorecard.yaml index 4c5b39bef5..4eb3499da1 100644 --- a/microsite/data/plugins/dora-scorecard.yaml +++ b/microsite/data/plugins/dora-scorecard.yaml @@ -7,3 +7,4 @@ description: Track and visualize DORA (DevOps Research and Assessment) metrics w documentation: https://github.com/zc149/backstage-plugin-dora-scorecard npmPackageName: '@jikwan/backstage-plugin-dora-scorecard' addedDate: '2025-12-29' +status: active diff --git a/microsite/data/plugins/dx.yaml b/microsite/data/plugins/dx.yaml index 6fbcb93f76..11d52a897d 100644 --- a/microsite/data/plugins/dx.yaml +++ b/microsite/data/plugins/dx.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/get-dx/backstage-plugin iconUrl: /img/dx-icon.png npmPackageName: '@get-dx/backstage-plugin' addedDate: '2024-03-05' +status: active diff --git a/microsite/data/plugins/dynatrace-managed.yaml b/microsite/data/plugins/dynatrace-managed.yaml index 11ae546d3d..66d1711b53 100644 --- a/microsite/data/plugins/dynatrace-managed.yaml +++ b/microsite/data/plugins/dynatrace-managed.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/dynatrace.svg npmPackageName: '@backstage-community/plugin-dynatrace' addedDate: '2022-06-23' +status: active diff --git a/microsite/data/plugins/dynatrace.yaml b/microsite/data/plugins/dynatrace.yaml index 995e21b8ff..bc3fcf05f9 100644 --- a/microsite/data/plugins/dynatrace.yaml +++ b/microsite/data/plugins/dynatrace.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Dynatrace/backstage-plugin iconUrl: /img/dynatrace.svg npmPackageName: '@dynatrace/backstage-plugin-dql' addedDate: '2024-03-22' +status: active diff --git a/microsite/data/plugins/end-of-life.yaml b/microsite/data/plugins/end-of-life.yaml index ddf4b26341..2f90f6cc01 100644 --- a/microsite/data/plugins/end-of-life.yaml +++ b/microsite/data/plugins/end-of-life.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/endoflife/docs/pluginIcon.png npmPackageName: '@dweber019/backstage-plugin-endoflife' addedDate: '2024-01-18' +status: active diff --git a/microsite/data/plugins/entity-feedback.yaml b/microsite/data/plugins/entity-feedback.yaml index a03537b4f7..248444c28f 100644 --- a/microsite/data/plugins/entity-feedback.yaml +++ b/microsite/data/plugins/entity-feedback.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/entity-feedback-logo.png npmPackageName: '@backstage-community/plugin-entity-feedback' addedDate: '2023-01-20' +status: active diff --git a/microsite/data/plugins/entity-validation.yaml b/microsite/data/plugins/entity-validation.yaml index 2ccb6deced..e5759c2f36 100644 --- a/microsite/data/plugins/entity-validation.yaml +++ b/microsite/data/plugins/entity-validation.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac npmPackageName: '@backstage-community/plugin-entity-validation' iconUrl: /img/entity-validation.svg addedDate: '2023-09-13' +status: active diff --git a/microsite/data/plugins/env0.yaml b/microsite/data/plugins/env0.yaml index 431873d347..74841cef67 100644 --- a/microsite/data/plugins/env0.yaml +++ b/microsite/data/plugins/env0.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/env0/env0-backstage-plugin iconUrl: https://avatars.githubusercontent.com/u/46656519?s=200&v=4 npmPackageName: '@env0/backstage-plugin-env0' addedDate: '2025-01-15' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/facets-cloud.yaml b/microsite/data/plugins/facets-cloud.yaml index 0f239a77ee..2f0b7d3319 100644 --- a/microsite/data/plugins/facets-cloud.yaml +++ b/microsite/data/plugins/facets-cloud.yaml @@ -10,3 +10,4 @@ documentation: https://github.com/Facets-cloud/facets-backstage-plugin iconUrl: /img/facets-cloud-logo.png npmPackageName: '@facets-cloud/backstage-plugin' addedDate: '2025-02-25' +status: active diff --git a/microsite/data/plugins/feedback.yaml b/microsite/data/plugins/feedback.yaml index fce9588653..e265952c31 100644 --- a/microsite/data/plugins/feedback.yaml +++ b/microsite/data/plugins/feedback.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/plugin-feedback-logo.svg npmPackageName: '@backstage-community/plugin-entity-feedback' addedDate: '2024-04-09' +status: active diff --git a/microsite/data/plugins/festive-fun.yaml b/microsite/data/plugins/festive-fun.yaml index 33995eca27..8baacac506 100644 --- a/microsite/data/plugins/festive-fun.yaml +++ b/microsite/data/plugins/festive-fun.yaml @@ -6,5 +6,7 @@ category: Humor description: Festive, seasonal, animations to spice up your instance of Backstage! documentation: https://github.com/benjmac/backstage-plugin-festive-fun/ iconUrl: https://raw.githubusercontent.com/benjmac/backstage-plugin-festive-fun/main/docs/festive-fun-logo.png -npmPackageName: 'backstage-plugin-festive-fun' +npmPackageName: backstage-plugin-festive-fun addedDate: '2023-11-20' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/firebase-functions.yaml b/microsite/data/plugins/firebase-functions.yaml index 176cfc1360..662f045245 100644 --- a/microsite/data/plugins/firebase-functions.yaml +++ b/microsite/data/plugins/firebase-functions.yaml @@ -8,3 +8,5 @@ documentation: https://roadie.io/backstage/plugins/firebase-functions/?utm_sourc iconUrl: https://roadie.io/images/logos/firebase.png npmPackageName: '@roadiehq/backstage-plugin-firebase-functions' addedDate: '2021-04-20' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/firefly.yaml b/microsite/data/plugins/firefly.yaml index 8a24920499..58d8b7a38b 100644 --- a/microsite/data/plugins/firefly.yaml +++ b/microsite/data/plugins/firefly.yaml @@ -8,3 +8,4 @@ documentation: https://docs.firefly.ai/firefly-docs/integrations/backstage iconUrl: https://avatars.githubusercontent.com/u/100200663?s=400&v=4 npmPackageName: '@fireflyai/backstage-plugin-firefly' addedDate: '2025-04-07' +status: active diff --git a/microsite/data/plugins/firehydrant.yaml b/microsite/data/plugins/firehydrant.yaml index f5ccd3629d..624d603da7 100644 --- a/microsite/data/plugins/firehydrant.yaml +++ b/microsite/data/plugins/firehydrant.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://github.com/backstage/community-plugins/blob/main/workspaces/firehydrant/plugins/firehydrant/doc/firehydrant_logo.png npmPackageName: '@backstage-community/plugin-firehydrant' addedDate: '2021-08-18' +status: active diff --git a/microsite/data/plugins/flagsmith.yaml b/microsite/data/plugins/flagsmith.yaml new file mode 100644 index 0000000000..42f9a8523a --- /dev/null +++ b/microsite/data/plugins/flagsmith.yaml @@ -0,0 +1,11 @@ +--- +title: Flagsmith +author: Flagsmith +authorUrl: https://www.flagsmith.com +category: Feature Flags +description: View Flagsmith feature flags and usage statistics inside Backstage. +documentation: https://docs.flagsmith.com/third-party-integrations/backstage +iconUrl: '/img/flagsmith.svg' +npmPackageName: '@flagsmith/backstage-plugin' +addedDate: '2026-02-26' +status: active diff --git a/microsite/data/plugins/flux.yaml b/microsite/data/plugins/flux.yaml index 91156f7ed3..253d5b4a64 100644 --- a/microsite/data/plugins/flux.yaml +++ b/microsite/data/plugins/flux.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/weaveworks/weaveworks-backstage/tree/main/plug iconUrl: https://raw.githubusercontent.com/fluxcd/website/main/assets/icons/logo.svg npmPackageName: '@weaveworksoss/backstage-plugin-flux' addedDate: '2023-08-23' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/fossa.yaml b/microsite/data/plugins/fossa.yaml index 34610c64e4..64ad72a026 100644 --- a/microsite/data/plugins/fossa.yaml +++ b/microsite/data/plugins/fossa.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://avatars0.githubusercontent.com/u/9543448?s=400&v=4 npmPackageName: '@backstage-community/plugin-fossa' addedDate: '2020-12-10' +status: active diff --git a/microsite/data/plugins/gcp-projects.yaml b/microsite/data/plugins/gcp-projects.yaml index 202f5b6e29..64cf5e243c 100644 --- a/microsite/data/plugins/gcp-projects.yaml +++ b/microsite/data/plugins/gcp-projects.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars1.githubusercontent.com/u/2810941?s=280&v=4 npmPackageName: '@backstage-community/plugin-gcp-projects' addedDate: '2021-01-20' +status: active diff --git a/microsite/data/plugins/git-release-manager.yaml b/microsite/data/plugins/git-release-manager.yaml index 244c88b981..4eab25c757 100644 --- a/microsite/data/plugins/git-release-manager.yaml +++ b/microsite/data/plugins/git-release-manager.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/git-release-manager-logo.svg npmPackageName: '@backstage-community/plugin-git-release-manager' addedDate: '2021-10-04' +status: active diff --git a/microsite/data/plugins/github-actions.yaml b/microsite/data/plugins/github-actions.yaml index 58e5d49051..55503f4fbb 100644 --- a/microsite/data/plugins/github-actions.yaml +++ b/microsite/data/plugins/github-actions.yaml @@ -4,7 +4,8 @@ author: Spotify authorUrl: https://github.com/spotify category: CI/CD description: GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. -documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/github-actions/plugins/github-actions +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/github/plugins/github-actions iconUrl: https://avatars2.githubusercontent.com/u/44036562?s=400&v=4 npmPackageName: '@backstage-community/plugin-github-actions' addedDate: '2021-01-20' +status: active diff --git a/microsite/data/plugins/github-codespaces.yaml b/microsite/data/plugins/github-codespaces.yaml index 243e7e5249..46ae467463 100644 --- a/microsite/data/plugins/github-codespaces.yaml +++ b/microsite/data/plugins/github-codespaces.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/adityasinghal26/backstage-plugins/tree/main/pl iconUrl: https://avatars.githubusercontent.com/u/9919?s=200&v=4 npmPackageName: '@adityasinghal26/plugin-github-codespaces' addedDate: '2023-12-30' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/github-copilot.yaml b/microsite/data/plugins/github-copilot.yaml index 5228d66ec5..2dd725e7be 100644 --- a/microsite/data/plugins/github-copilot.yaml +++ b/microsite/data/plugins/github-copilot.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://platformnewsletter.blob.core.windows.net/$web/copilot.png npmPackageName: '@backstage-community/plugin-copilot' addedDate: '2024-10-21' +status: active diff --git a/microsite/data/plugins/github-insights.yaml b/microsite/data/plugins/github-insights.yaml index 87a636f006..deb9fe4ff5 100644 --- a/microsite/data/plugins/github-insights.yaml +++ b/microsite/data/plugins/github-insights.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/github-insights/?utm_source=b iconUrl: https://roadie.io/images/logos/insights.png npmPackageName: '@roadiehq/backstage-plugin-github-insights' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/github-pull-requests-board.yaml b/microsite/data/plugins/github-pull-requests-board.yaml index 51c9f1a2e4..efbe4e1fda 100644 --- a/microsite/data/plugins/github-pull-requests-board.yaml +++ b/microsite/data/plugins/github-pull-requests-board.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/github-pull-requests-board-logo.svg npmPackageName: '@backstage-community/plugin-github-pull-requests-board' addedDate: '2022-05-10' +status: active diff --git a/microsite/data/plugins/github-pull-requests.yaml b/microsite/data/plugins/github-pull-requests.yaml index 57276a634b..122e4a5189 100644 --- a/microsite/data/plugins/github-pull-requests.yaml +++ b/microsite/data/plugins/github-pull-requests.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/github-pull-requests/?utm_sou iconUrl: https://roadie.io/images/logos/github.png npmPackageName: '@roadiehq/backstage-plugin-github-pull-requests' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/github-workflows.yaml b/microsite/data/plugins/github-workflows.yaml index 4fb5d95c71..613b84ff0b 100644 --- a/microsite/data/plugins/github-workflows.yaml +++ b/microsite/data/plugins/github-workflows.yaml @@ -8,3 +8,4 @@ documentation: https://platform.vee.codes/plugin/github-workflows/ iconUrl: https://veecode-platform.github.io/support/imgs/logo_2.svg npmPackageName: '@veecode-platform/backstage-plugin-github-workflows' addedDate: '2023-09-12' +status: active diff --git a/microsite/data/plugins/gitlab-pipelines.yaml b/microsite/data/plugins/gitlab-pipelines.yaml index df2ea745bd..e8d236d54a 100644 --- a/microsite/data/plugins/gitlab-pipelines.yaml +++ b/microsite/data/plugins/gitlab-pipelines.yaml @@ -8,3 +8,4 @@ documentation: https://platform.vee.codes/plugin/gitlab-pipelines/ iconUrl: https://veecode-platform.github.io/support/imgs/logo_1.svg npmPackageName: '@veecode-platform/backstage-plugin-gitlab-pipelines' addedDate: '2023-09-27' +status: active diff --git a/microsite/data/plugins/gitlab.yaml b/microsite/data/plugins/gitlab.yaml index 356875cdd0..5afd2009b5 100644 --- a/microsite/data/plugins/gitlab.yaml +++ b/microsite/data/plugins/gitlab.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/immobiliare/backstage-plugin-gitlab iconUrl: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png npmPackageName: '@immobiliarelabs/backstage-plugin-gitlab' addedDate: '2021-08-17' +status: active diff --git a/microsite/data/plugins/gitops-cluster.yaml b/microsite/data/plugins/gitops-cluster.yaml index acfb79a4e3..66eca0b73e 100644 --- a/microsite/data/plugins/gitops-cluster.yaml +++ b/microsite/data/plugins/gitops-cluster.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@backstage-community/plugin-gitops-profiles' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/glean.yaml b/microsite/data/plugins/glean.yaml index 057ec2f4fd..b33248854e 100644 --- a/microsite/data/plugins/glean.yaml +++ b/microsite/data/plugins/glean.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/beejiujitsu/wealthsimple-backstage-plugins/tre iconUrl: /img/glean-logo-circular-white.png npmPackageName: '@beejiujitsu/backstage-plugin-glean-backend' addedDate: '2024-12-30' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/gocd.yaml b/microsite/data/plugins/gocd.yaml index 34b9931242..c5b483df5f 100644 --- a/microsite/data/plugins/gocd.yaml +++ b/microsite/data/plugins/gocd.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://www.gocd.org/assets/images/go_logo-5b5ca9e1.svg npmPackageName: '@backstage-community/plugin-gocd' addedDate: '2022-01-15' +status: active diff --git a/microsite/data/plugins/grafana.yaml b/microsite/data/plugins/grafana.yaml index 62e287fb94..f3b9261d46 100644 --- a/microsite/data/plugins/grafana.yaml +++ b/microsite/data/plugins/grafana.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://avatars.githubusercontent.com/u/7195757?s=200&v=4 npmPackageName: '@backstage-community/plugin-grafana' addedDate: '2021-10-11' +status: active diff --git a/microsite/data/plugins/graphiql.yaml b/microsite/data/plugins/graphiql.yaml index 2eff5588df..8d0a37cb65 100644 --- a/microsite/data/plugins/graphiql.yaml +++ b/microsite/data/plugins/graphiql.yaml @@ -5,6 +5,7 @@ authorUrl: https://github.com/spotify category: Debugging description: Integrates GraphiQL as a tool to browse GraphQL API endpoints inside Backstage. documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/graphiql/plugins/graphiql -iconUrl: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1024px-GraphQL_Logo.svg.png +iconUrl: https://upload.wikimedia.org/wikipedia/commons/1/17/GraphQL_Logo.svg npmPackageName: '@backstage-community/plugin-graphiql' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/graphql-catalog.yaml b/microsite/data/plugins/graphql-catalog.yaml index 2b428429b8..4c848269eb 100644 --- a/microsite/data/plugins/graphql-catalog.yaml +++ b/microsite/data/plugins/graphql-catalog.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/thefrontside/playhouse/blob/main/plugins/graph iconUrl: https://raw.githubusercontent.com/thefrontside/frontside.com/production/legacy/src/img/frontside-logo.png npmPackageName: '@frontside/backstage-plugin-graphql-backend' addedDate: '2024-05-01' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/graphql-voyager.yaml b/microsite/data/plugins/graphql-voyager.yaml index ec3cbe9113..0a5379a960 100644 --- a/microsite/data/plugins/graphql-voyager.yaml +++ b/microsite/data/plugins/graphql-voyager.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://res.cloudinary.com/apideck/image/upload/v1612724234/icons/graphql-voyager.png npmPackageName: '@backstage-community/plugin-graphql-voyager' addedDate: '2023-01-27' +status: active diff --git a/microsite/data/plugins/gravatar.yaml b/microsite/data/plugins/gravatar.yaml index e32369fa54..c5903e157d 100644 --- a/microsite/data/plugins/gravatar.yaml +++ b/microsite/data/plugins/gravatar.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/pl iconUrl: https://gravatar.com/avatar/gravatar npmPackageName: '@roadiehq/catalog-backend-module-gravatar' addedDate: '2024-08-26' +status: active diff --git a/microsite/data/plugins/grpc-playground.yaml b/microsite/data/plugins/grpc-playground.yaml index b0f12d4aa2..a65d57c54c 100644 --- a/microsite/data/plugins/grpc-playground.yaml +++ b/microsite/data/plugins/grpc-playground.yaml @@ -6,5 +6,7 @@ category: Discovery description: Easily view and test your gRPC API with a GUI Client, inspired from BloomRPC application. documentation: https://github.com/zalopay-oss/backstage-grpc-playground iconUrl: https://raw.githubusercontent.com/zalopay-oss/backstage-grpc-playground/main/images/gprc-logo.png -npmPackageName: 'backstage-grpc-playground' +npmPackageName: backstage-grpc-playground addedDate: '2022-06-08' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/harbor.yaml b/microsite/data/plugins/harbor.yaml index 344c4524c9..f874f7e83a 100644 --- a/microsite/data/plugins/harbor.yaml +++ b/microsite/data/plugins/harbor.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/harbor/icon/color/harbor-icon-color.svg npmPackageName: '@bestsellerit/backstage-plugin-harbor' addedDate: '2022-06-23' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/harness-ccm.yaml b/microsite/data/plugins/harness-ccm.yaml index a7bafb6cc7..eb978c6088 100644 --- a/microsite/data/plugins/harness-ccm.yaml +++ b/microsite/data/plugins/harness-ccm.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-harness-ccm' addedDate: '2024-09-27' +status: active diff --git a/microsite/data/plugins/harness-chaos.yaml b/microsite/data/plugins/harness-chaos.yaml index 047b8d246f..cfb67d3d20 100644 --- a/microsite/data/plugins/harness-chaos.yaml +++ b/microsite/data/plugins/harness-chaos.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-harness-chaos' addedDate: '2024-06-25' +status: active diff --git a/microsite/data/plugins/harness-ci-cd.yaml b/microsite/data/plugins/harness-ci-cd.yaml index e72e91e80f..96740e2927 100644 --- a/microsite/data/plugins/harness-ci-cd.yaml +++ b/microsite/data/plugins/harness-ci-cd.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-ci-cd' addedDate: '2022-12-08' +status: active diff --git a/microsite/data/plugins/harness-feature-flags.yaml b/microsite/data/plugins/harness-feature-flags.yaml index 4a38a5e6ae..048407b6d7 100644 --- a/microsite/data/plugins/harness-feature-flags.yaml +++ b/microsite/data/plugins/harness-feature-flags.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-feature-flags' addedDate: '2023-03-01' +status: active diff --git a/microsite/data/plugins/harness-iacm.yaml b/microsite/data/plugins/harness-iacm.yaml index 97c9d19fda..7a6bf0a6d6 100644 --- a/microsite/data/plugins/harness-iacm.yaml +++ b/microsite/data/plugins/harness-iacm.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-harness-iacm' addedDate: '2024-07-03' +status: active diff --git a/microsite/data/plugins/harness-srm.yaml b/microsite/data/plugins/harness-srm.yaml index 9153b56ea5..d687832232 100644 --- a/microsite/data/plugins/harness-srm.yaml +++ b/microsite/data/plugins/harness-srm.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/ha iconUrl: https://static.harness.io/ng-static/images/favicon.png npmPackageName: '@harnessio/backstage-plugin-harness-srm' addedDate: '2023-11-17' +status: active diff --git a/microsite/data/plugins/hashicorp-terraform.yaml b/microsite/data/plugins/hashicorp-terraform.yaml index c254667015..3b996be7b3 100644 --- a/microsite/data/plugins/hashicorp-terraform.yaml +++ b/microsite/data/plugins/hashicorp-terraform.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/globallogicuki/globallogic-backstage-plugins/t iconUrl: /img/terraform-logo.svg npmPackageName: '@globallogicuki/backstage-plugin-terraform' addedDate: '2024-07-17' +status: active diff --git a/microsite/data/plugins/hcp-consul.yaml b/microsite/data/plugins/hcp-consul.yaml index 6dabad582e..5501e0d191 100644 --- a/microsite/data/plugins/hcp-consul.yaml +++ b/microsite/data/plugins/hcp-consul.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/hashicorp-forge/backstage-plugin-hcp-consul/tr iconUrl: /img/hcp-consul.svg npmPackageName: '@hashicorp/plugin-hcp-consul' addedDate: '2023-11-24' +status: active diff --git a/microsite/data/plugins/hetzner-cloud.yaml b/microsite/data/plugins/hetzner-cloud.yaml index 6f1fbe3f25..36e9172e9f 100644 --- a/microsite/data/plugins/hetzner-cloud.yaml +++ b/microsite/data/plugins/hetzner-cloud.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/gluobe/hetzner-backstage-plugin/blob/main/READ iconUrl: https://avatars.githubusercontent.com/u/18439789?s=200&v=4 npmPackageName: '@gluo-nv/backstage-plugin-hetzner' addedDate: '2025-06-03' +status: active diff --git a/microsite/data/plugins/holiday-tracker.yaml b/microsite/data/plugins/holiday-tracker.yaml index 3232088582..20cbea8057 100644 --- a/microsite/data/plugins/holiday-tracker.yaml +++ b/microsite/data/plugins/holiday-tracker.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Infosys/holiday-tracker-plugin/blob/main/plugi iconUrl: https://github.com/Infosys/holiday-tracker-plugin/blob/main/plugins/holiday-tracker/src/docs/holiday-tracker-plugin.logo.png?raw=true npmPackageName: '@infosys_ltd/holiday-tracker-plugin' addedDate: '2025-07-23' +status: active diff --git a/microsite/data/plugins/home.yaml b/microsite/data/plugins/home.yaml index 0ca2de56f2..b2b45b9a11 100644 --- a/microsite/data/plugins/home.yaml +++ b/microsite/data/plugins/home.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/home/R iconUrl: /img/home.png npmPackageName: '@backstage/plugin-home' addedDate: '2021-08-31' +status: active diff --git a/microsite/data/plugins/hoop.yaml b/microsite/data/plugins/hoop.yaml index 0e9b4c8aa3..062b1daa74 100644 --- a/microsite/data/plugins/hoop.yaml +++ b/microsite/data/plugins/hoop.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/hoophq/backstage-plugin iconUrl: https://avatars.githubusercontent.com/u/113131551?s=200&v=4 npmPackageName: '@hoophq/backstage-plugin' addedDate: '2022-12-20' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml index a94ca431e6..fc1ddc7d4f 100644 --- a/microsite/data/plugins/humanitec.yaml +++ b/microsite/data/plugins/humanitec.yaml @@ -10,3 +10,4 @@ documentation: https://github.com/humanitec/humanitec-backstage-plugins iconUrl: /img/humanitec-logo.png npmPackageName: '@humanitec/backstage-plugin' addedDate: '2022-06-22' +status: active diff --git a/microsite/data/plugins/ibm-apic-backend.yaml b/microsite/data/plugins/ibm-apic-backend.yaml index a048f68f4e..1903a0dc1e 100644 --- a/microsite/data/plugins/ibm-apic-backend.yaml +++ b/microsite/data/plugins/ibm-apic-backend.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/croz-ltd/apic-backend-plugin?utm_source=backst iconUrl: https://croz.net/app/uploads/2024/05/apple-touch-icon.png npmPackageName: '@croz/plugin-ibm-apic-backend' addedDate: '2024-03-01' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml index 38cfc69acf..1ebf2b9d9f 100644 --- a/microsite/data/plugins/ilert.yaml +++ b/microsite/data/plugins/ilert.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4 npmPackageName: '@backstage-community/plugin-ilert' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/infisical.yaml b/microsite/data/plugins/infisical.yaml index 41134c0b42..7aa08273fd 100644 --- a/microsite/data/plugins/infisical.yaml +++ b/microsite/data/plugins/infisical.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Infisical/infisical-backstage-plugin iconUrl: https://avatars.githubusercontent.com/u/107880645?s=200&v=4 npmPackageName: '@infisical/backstage-plugin-infisical' addedDate: '2025-04-11' +status: active diff --git a/microsite/data/plugins/infracost.yaml b/microsite/data/plugins/infracost.yaml index 80d9c9f545..33f870d843 100644 --- a/microsite/data/plugins/infracost.yaml +++ b/microsite/data/plugins/infracost.yaml @@ -8,3 +8,4 @@ documentation: https://platform.vee.codes/plugin/Infracost iconUrl: https://veecode-platform.github.io/support/imgs/logo_5.svg npmPackageName: '@veecode-platform/backstage-plugin-infracost' addedDate: '2024-07-25' +status: active diff --git a/microsite/data/plugins/infrawallet.yaml b/microsite/data/plugins/infrawallet.yaml index ee553e4283..77e2eefaf8 100644 --- a/microsite/data/plugins/infrawallet.yaml +++ b/microsite/data/plugins/infrawallet.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/electrolux-oss/infrawallet/blob/main/README.md iconUrl: /img/infrawallet-logo.png npmPackageName: '@electrolux-oss/plugin-infrawallet' addedDate: '2024-06-03' +status: active diff --git a/microsite/data/plugins/jaegertracing.yaml b/microsite/data/plugins/jaegertracing.yaml index ae423995e8..ba2b40daee 100644 --- a/microsite/data/plugins/jaegertracing.yaml +++ b/microsite/data/plugins/jaegertracing.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://avatars.githubusercontent.com/u/28545596?s=200&v=4 npmPackageName: '@backstage-community/plugin-jaeger' addedDate: '2024-12-21' +status: active diff --git a/microsite/data/plugins/jenkins-actions.yaml b/microsite/data/plugins/jenkins-actions.yaml index 97546a3e1f..55b22a14e8 100644 --- a/microsite/data/plugins/jenkins-actions.yaml +++ b/microsite/data/plugins/jenkins-actions.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/jenkins.png npmPackageName: '@backstage-community/plugin-scaffolder-backend-module-jenkins' addedDate: '2024-12-06' +status: active diff --git a/microsite/data/plugins/jenkins.yaml b/microsite/data/plugins/jenkins.yaml index b347f67d26..b723362db4 100644 --- a/microsite/data/plugins/jenkins.yaml +++ b/microsite/data/plugins/jenkins.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://img.icons8.com/color/1600/jenkins.png npmPackageName: '@backstage-community/plugin-jenkins' addedDate: '2021-01-20' +status: active diff --git a/microsite/data/plugins/jfrog-artifactory-libs.yaml b/microsite/data/plugins/jfrog-artifactory-libs.yaml index 22583cbfbd..a97d975c59 100644 --- a/microsite/data/plugins/jfrog-artifactory-libs.yaml +++ b/microsite/data/plugins/jfrog-artifactory-libs.yaml @@ -3,11 +3,10 @@ title: JFrog Artifactory Libs plugin author: Vity authorUrl: https://github.com/Vity01 category: Discovery -description: - frontend plugin provides a simple way to display generated artifact (library) details like - - group, artifact, repository, what is the latest version, and it simply - allows to copy library definition for the package managers. +description: frontend plugin provides a simple way to display generated artifact (library) details like - group, artifact, repository, what is the latest version, and it simply allows to copy library definition for the package managers. documentation: https://github.com/Vity01/backstage-jfrog-artifactory-libs iconUrl: https://raw.githubusercontent.com/Vity01/backstage-jfrog-artifactory-libs/main/doc/artifactory.svg -npmPackageName: 'backstage-plugin-jfrog-artifactory-libs' +npmPackageName: backstage-plugin-jfrog-artifactory-libs addedDate: '2023-08-14' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/jfrog-artifactory.yaml b/microsite/data/plugins/jfrog-artifactory.yaml index b4f1ef567e..b4e921f9c5 100644 --- a/microsite/data/plugins/jfrog-artifactory.yaml +++ b/microsite/data/plugins/jfrog-artifactory.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/jfrog-artifactory.svg npmPackageName: '@backstage-community/plugin-jfrog-artifactory' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/jira-dashboard.yaml b/microsite/data/plugins/jira-dashboard.yaml index 8cb9351c8d..e111248f55 100644 --- a/microsite/data/plugins/jira-dashboard.yaml +++ b/microsite/data/plugins/jira-dashboard.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main iconUrl: https://raw.githubusercontent.com/AxisCommunications/backstage-plugins/main/plugins/jira-dashboard/media/jira-logo.png npmPackageName: '@axis-backstage/plugin-jira-dashboard' addedDate: '2023-12-07' +status: active diff --git a/microsite/data/plugins/jira.yaml b/microsite/data/plugins/jira.yaml index 6c803bcd68..0ba6c96fac 100644 --- a/microsite/data/plugins/jira.yaml +++ b/microsite/data/plugins/jira.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/jira/?utm_source=backstage.io iconUrl: https://roadie.io/images/logos/jira.png npmPackageName: '@roadiehq/backstage-plugin-jira' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/k8sgpt.yaml b/microsite/data/plugins/k8sgpt.yaml index f509009a4f..c2d6c6a3d8 100644 --- a/microsite/data/plugins/k8sgpt.yaml +++ b/microsite/data/plugins/k8sgpt.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/suxess-it/backstage-plugin-k8sgpt/blob/main/RE iconUrl: https://raw.githubusercontent.com/k8sgpt-ai/k8sgpt/1a81227d6148be59b7b9ae4e9ae5e2d9a5b7a9ae/images/banner-white.png npmPackageName: '@suxess-it/backstage-plugin-k8sgpt' addedDate: '2023-06-05' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/kafka.yaml b/microsite/data/plugins/kafka.yaml index b3adecc623..e7d7ce443a 100644 --- a/microsite/data/plugins/kafka.yaml +++ b/microsite/data/plugins/kafka.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://kafka.apache.org/images/apache-kafka.png npmPackageName: '@backstage-community/plugin-kafka' addedDate: '2021-01-21' +status: active diff --git a/microsite/data/plugins/keycloak.yaml b/microsite/data/plugins/keycloak.yaml index 4136f50f95..d7fad6a0ae 100644 --- a/microsite/data/plugins/keycloak.yaml +++ b/microsite/data/plugins/keycloak.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/keycloak.svg npmPackageName: '@backstage-community/plugin-catalog-backend-module-keycloak' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/kiali.yaml b/microsite/data/plugins/kiali.yaml index 86299c5045..5c27a3b776 100644 --- a/microsite/data/plugins/kiali.yaml +++ b/microsite/data/plugins/kiali.yaml @@ -8,3 +8,5 @@ documentation: https://janus-idp.io/plugins/kiali iconUrl: /img/kiali.svg npmPackageName: '@janus-idp/backstage-plugin-kiali' addedDate: '2023-07-25' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/knative-event-mesh.yaml b/microsite/data/plugins/knative-event-mesh.yaml index 6ecb219b32..2a9e4cb17c 100644 --- a/microsite/data/plugins/knative-event-mesh.yaml +++ b/microsite/data/plugins/knative-event-mesh.yaml @@ -8,3 +8,4 @@ documentation: https://knative.dev/docs/install/installing-backstage-plugins/ npmPackageName: '@knative-extensions/plugin-knative-event-mesh-backend' iconUrl: https://raw.githubusercontent.com/knative/community/refs/heads/main/icons/logo.svg addedDate: '2024-12-16' +status: active diff --git a/microsite/data/plugins/kong-service-manager.yaml b/microsite/data/plugins/kong-service-manager.yaml index 078621112f..9d5688b12b 100644 --- a/microsite/data/plugins/kong-service-manager.yaml +++ b/microsite/data/plugins/kong-service-manager.yaml @@ -8,3 +8,4 @@ documentation: https://platform.vee.codes/plugin/kong-service-manager/ iconUrl: https://veecode-platform.github.io/support/imgs/logo_3.svg npmPackageName: '@veecode-platform/plugin-kong-service-manager' addedDate: '2024-05-27' +status: active diff --git a/microsite/data/plugins/kpt-config-as-data.yaml b/microsite/data/plugins/kpt-config-as-data.yaml index af157a70c3..c97996e2b0 100644 --- a/microsite/data/plugins/kpt-config-as-data.yaml +++ b/microsite/data/plugins/kpt-config-as-data.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/GoogleContainerTools/kpt-backstage-plugins/tre iconUrl: https://github.com/GoogleContainerTools/kpt/blob/main/logo/KptLogoSmall.png?raw=true npmPackageName: '@kpt/backstage-plugin-cad' addedDate: '2022-05-13' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/kubecost.yaml b/microsite/data/plugins/kubecost.yaml index f1f4ff1218..4e6b388c18 100644 --- a/microsite/data/plugins/kubecost.yaml +++ b/microsite/data/plugins/kubecost.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/suxess-it/backstage-plugin-kubecost/blob/main/ iconUrl: https://avatars.githubusercontent.com/u/45108136?s=200&v=4 npmPackageName: '@suxess-it/backstage-plugin-kubecost' addedDate: '2023-06-29' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/kubelog.yaml b/microsite/data/plugins/kubelog.yaml index 0cad3e993b..2ab4c8c89d 100644 --- a/microsite/data/plugins/kubelog.yaml +++ b/microsite/data/plugins/kubelog.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/jfvilas/kubelog iconUrl: https://raw.githubusercontent.com/jfvilas/kubelog/master/src/assets/kubelog-logo.png npmPackageName: '@jfvilas/plugin-kubelog' addedDate: '2024-08-25' +status: active diff --git a/microsite/data/plugins/kubernetes-gpt-analyzer.yaml b/microsite/data/plugins/kubernetes-gpt-analyzer.yaml index fd44a11ad0..5c111bec70 100644 --- a/microsite/data/plugins/kubernetes-gpt-analyzer.yaml +++ b/microsite/data/plugins/kubernetes-gpt-analyzer.yaml @@ -8,3 +8,4 @@ documentation: https://platform.vee.codes/plugin/kubernetes-gpt-analyzer/ iconUrl: https://veecode-platform.github.io/support/imgs/logo_4.svg npmPackageName: '@veecode-platform/backstage-plugin-kubernetes-gpt-analyzer' addedDate: '2024-07-31' +status: active diff --git a/microsite/data/plugins/kubernetes-ingestor.yaml b/microsite/data/plugins/kubernetes-ingestor.yaml index 006333e990..6c3ae5b24f 100644 --- a/microsite/data/plugins/kubernetes-ingestor.yaml +++ b/microsite/data/plugins/kubernetes-ingestor.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugin iconUrl: https://avatars.githubusercontent.com/u/13629408 npmPackageName: '@terasky/backstage-plugin-kubernetes-ingestor' addedDate: '2024-12-30' +status: active diff --git a/microsite/data/plugins/kubernetes-provider.yaml b/microsite/data/plugins/kubernetes-provider.yaml index b23ab03caa..75f1f0780a 100644 --- a/microsite/data/plugins/kubernetes-provider.yaml +++ b/microsite/data/plugins/kubernetes-provider.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/AntoineDao/backstage-provider-kubernetes#readm iconUrl: https://avatars.githubusercontent.com/u/13629408 npmPackageName: '@antoinedao/backstage-provider-kubernetes' addedDate: '2023-04-10' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/kubevela.yaml b/microsite/data/plugins/kubevela.yaml index 7baf6c668b..ab5acc1cb1 100644 --- a/microsite/data/plugins/kubevela.yaml +++ b/microsite/data/plugins/kubevela.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/kubevela-contrib/backstage-plugin-kubevela iconUrl: https://kubevela.io/img/logo.svg npmPackageName: '@oamdev/plugin-kubevela-backend' addedDate: '2023-02-12' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/kwirthlog.yaml b/microsite/data/plugins/kwirthlog.yaml index 49863c2d4e..ac8eafe036 100644 --- a/microsite/data/plugins/kwirthlog.yaml +++ b/microsite/data/plugins/kwirthlog.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/jfvilas/plugin-kwirth-log iconUrl: https://raw.githubusercontent.com/jfvilas/plugin-kwirth-log/master/src/assets/kwirthlog-logo.png npmPackageName: '@jfvilas/plugin-kwirth-log' addedDate: '2025-09-18' +status: active diff --git a/microsite/data/plugins/kwirthmetrics.yaml b/microsite/data/plugins/kwirthmetrics.yaml index 9a8c069eda..48bf963ac2 100644 --- a/microsite/data/plugins/kwirthmetrics.yaml +++ b/microsite/data/plugins/kwirthmetrics.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/jfvilas/plugin-kwirth-metrics iconUrl: https://raw.githubusercontent.com/jfvilas/plugin-kwirth-metrics/master/src/assets/kwirthmetrics-logo.png npmPackageName: '@jfvilas/plugin-kwirth-metrics' addedDate: '2025-09-14' +status: active diff --git a/microsite/data/plugins/kyverno.yaml b/microsite/data/plugins/kyverno.yaml index 63554d02fe..2137262fcb 100644 --- a/microsite/data/plugins/kyverno.yaml +++ b/microsite/data/plugins/kyverno.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugin iconUrl: https://github.com/cncf/artwork/blob/main/projects/kyverno/icon/color/kyverno-icon-color.png?raw=true npmPackageName: '@terasky/backstage-plugin-kyverno-policy-reports' addedDate: '2025-01-05' +status: active diff --git a/microsite/data/plugins/launchdarkly.yaml b/microsite/data/plugins/launchdarkly.yaml index 0a3415d736..c97550147e 100644 --- a/microsite/data/plugins/launchdarkly.yaml +++ b/microsite/data/plugins/launchdarkly.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/launchd iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@roadiehq/backstage-plugin-launchdarkly' addedDate: '2024-07-27' +status: active diff --git a/microsite/data/plugins/ldap-auth.yaml b/microsite/data/plugins/ldap-auth.yaml index e3c917d427..3b56deceb5 100644 --- a/microsite/data/plugins/ldap-auth.yaml +++ b/microsite/data/plugins/ldap-auth.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/immobiliare/backstage-plugin-ldap-auth/blob/ma iconUrl: https://avatars.githubusercontent.com/u/10090828 npmPackageName: '@immobiliarelabs/backstage-plugin-ldap-auth' addedDate: '2022-09-05' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/library-check.yaml b/microsite/data/plugins/library-check.yaml index 68489d676f..2150fb0d0b 100644 --- a/microsite/data/plugins/library-check.yaml +++ b/microsite/data/plugins/library-check.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/anakzr/backstage-plugin-library-check iconUrl: /img/library-check-logo.png npmPackageName: '@anakz/backstage-plugin-library-check' addedDate: '2024-03-11' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/lighthouse.yaml b/microsite/data/plugins/lighthouse.yaml index 860efb729d..f9d70e325a 100644 --- a/microsite/data/plugins/lighthouse.yaml +++ b/microsite/data/plugins/lighthouse.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://seeklogo.com/images/G/google-lighthouse-logo-1C7FA08580-seeklogo.com.png npmPackageName: '@backstage-community/plugin-lighthouse' addedDate: '2021-01-20' +status: active diff --git a/microsite/data/plugins/linguist.yaml b/microsite/data/plugins/linguist.yaml index 15b79b3682..19cf007f1f 100644 --- a/microsite/data/plugins/linguist.yaml +++ b/microsite/data/plugins/linguist.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/linguist.svg npmPackageName: '@backstage-community/plugin-linguist' addedDate: '2023-06-17' +status: active diff --git a/microsite/data/plugins/litmus.yaml b/microsite/data/plugins/litmus.yaml index d27bc4c7d7..045c2ce381 100644 --- a/microsite/data/plugins/litmus.yaml +++ b/microsite/data/plugins/litmus.yaml @@ -6,5 +6,7 @@ category: Chaos Engineering description: This plugin lets you view the status of Litmus resources and launch Chaos Experiments directly inside Backstage. documentation: https://github.com/litmuschaos/backstage-plugin/blob/master/README.md iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/litmus/icon/color/litmus-icon-color.svg -npmPackageName: 'backstage-plugin-litmus' +npmPackageName: backstage-plugin-litmus addedDate: '2023-10-06' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/mend.yaml b/microsite/data/plugins/mend.yaml index f7cf12e374..3ae16d50c9 100644 --- a/microsite/data/plugins/mend.yaml +++ b/microsite/data/plugins/mend.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://avatars.githubusercontent.com/u/105765982?s=200&v=4 npmPackageName: '@backstage-community/plugin-mend' addedDate: '2024-10-15' +status: active diff --git a/microsite/data/plugins/mia-platform.yaml b/microsite/data/plugins/mia-platform.yaml index 629b2d5ce4..7e7fa23f78 100644 --- a/microsite/data/plugins/mia-platform.yaml +++ b/microsite/data/plugins/mia-platform.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/mia-platform/backstage-plugin/blob/main/README iconUrl: /img/mia-platform.png npmPackageName: '@mia-platform/backstage-plugin-frontend' addedDate: '2024-04-12' +status: active diff --git a/microsite/data/plugins/microcks.yaml b/microsite/data/plugins/microcks.yaml index 9effacda74..3bab01f339 100644 --- a/microsite/data/plugins/microcks.yaml +++ b/microsite/data/plugins/microcks.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/microcks/microcks-backstage-provider iconUrl: https://raw.githubusercontent.com/microcks/.github/refs/heads/main/assets/microcks-logo-blue.png npmPackageName: '@microcks/microcks-backstage-provider' addedDate: '2023-04-06' +status: active diff --git a/microsite/data/plugins/microsoft-calendar.yaml b/microsite/data/plugins/microsoft-calendar.yaml index 1a0a203420..8803c503ec 100644 --- a/microsite/data/plugins/microsoft-calendar.yaml +++ b/microsite/data/plugins/microsoft-calendar.yaml @@ -9,3 +9,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@backstage-community/plugin-microsoft-calendar' addedDate: '2023-03-15' +status: active diff --git a/microsite/data/plugins/microsoft-forms.yaml b/microsite/data/plugins/microsoft-forms.yaml index 91be292ccd..ea1d11899e 100644 --- a/microsite/data/plugins/microsoft-forms.yaml +++ b/microsite/data/plugins/microsoft-forms.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/zcmander/backstage-plugin-msforms iconUrl: /img/microsoft-forms.png npmPackageName: '@zcmander/backstage-plugin-msforms' addedDate: '2023-04-11' +status: active diff --git a/microsite/data/plugins/n8n.yaml b/microsite/data/plugins/n8n.yaml new file mode 100644 index 0000000000..7898c1efe4 --- /dev/null +++ b/microsite/data/plugins/n8n.yaml @@ -0,0 +1,11 @@ +--- +title: n8n +author: André Ahlert Junior +authorUrl: https://github.com/andreahlert +category: Other +description: n8n workflow automation — view workflows and execution history on Backstage entity pages. +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/n8n/plugins/n8n +iconUrl: /img/logo-gradient-on-dark.svg +npmPackageName: '@backstage-community/plugin-n8n' +addedDate: '2026-02-15' +status: active diff --git a/microsite/data/plugins/new-relic.yaml b/microsite/data/plugins/new-relic.yaml index a2defdd007..98f7214c45 100644 --- a/microsite/data/plugins/new-relic.yaml +++ b/microsite/data/plugins/new-relic.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://www.mulesoft.com/sites/default/files/2018-10/New_relic.png npmPackageName: '@backstage-community/plugin-newrelic' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/newrelic-dashboard.yaml b/microsite/data/plugins/newrelic-dashboard.yaml index 2b0aeb1261..4c95c87ec7 100644 --- a/microsite/data/plugins/newrelic-dashboard.yaml +++ b/microsite/data/plugins/newrelic-dashboard.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: https://newrelic.com/themes/custom/erno/assets/mediakit/new_relic_logo_vertical_white.svg npmPackageName: '@backstage-community/plugin-newrelic-dashboard' addedDate: '2021-12-23' +status: active diff --git a/microsite/data/plugins/nexus-repository-manager.yaml b/microsite/data/plugins/nexus-repository-manager.yaml index 77b62c841d..5b7dca269d 100644 --- a/microsite/data/plugins/nexus-repository-manager.yaml +++ b/microsite/data/plugins/nexus-repository-manager.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/nexus-repository-manager.svg npmPackageName: '@backstage-community/plugin-nexus-repository-manager' addedDate: '2023-10-25' +status: active diff --git a/microsite/data/plugins/nobl9.yaml b/microsite/data/plugins/nobl9.yaml index ee447fd626..b099b1dd79 100644 --- a/microsite/data/plugins/nobl9.yaml +++ b/microsite/data/plugins/nobl9.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/nobl9/nobl9-backstage-plugin/blob/main/README. iconUrl: /img/nobl9.svg npmPackageName: '@nobl9/nobl9-backstage-plugin' addedDate: '2023-12-15' +status: active diff --git a/microsite/data/plugins/npm.yaml b/microsite/data/plugins/npm.yaml index 74b543ccc1..68e569e4ed 100644 --- a/microsite/data/plugins/npm.yaml +++ b/microsite/data/plugins/npm.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@backstage-community/plugin-npm' addedDate: '2024-11-29' +status: active diff --git a/microsite/data/plugins/ocm.yaml b/microsite/data/plugins/ocm.yaml index 1756029b27..38cc897339 100644 --- a/microsite/data/plugins/ocm.yaml +++ b/microsite/data/plugins/ocm.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/ocm.svg npmPackageName: '@backstage-community/plugin-ocm' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/octopus-deploy.yaml b/microsite/data/plugins/octopus-deploy.yaml index eb19f60e72..90c3a009f3 100644 --- a/microsite/data/plugins/octopus-deploy.yaml +++ b/microsite/data/plugins/octopus-deploy.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/octopus-deploy.svg npmPackageName: '@backstage-community/plugin-octopus-deploy' addedDate: '2023-02-24' +status: active diff --git a/microsite/data/plugins/okta-entity-providers.yaml b/microsite/data/plugins/okta-entity-providers.yaml index 4544c9eae7..55e88c1b12 100644 --- a/microsite/data/plugins/okta-entity-providers.yaml +++ b/microsite/data/plugins/okta-entity-providers.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/pl iconUrl: https://roadie.io/images/logos/okta.png npmPackageName: '@roadiehq/catalog-backend-module-okta' addedDate: '2022-07-26' +status: active diff --git a/microsite/data/plugins/opa-permissions-wrapper.yaml b/microsite/data/plugins/opa-permissions-wrapper.yaml index 64af859dbc..f33c4d68c9 100644 --- a/microsite/data/plugins/opa-permissions-wrapper.yaml +++ b/microsite/data/plugins/opa-permissions-wrapper.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Parsifal-M/backstage-opa-plugins/blob/main/plu iconUrl: /img/opapermlogo.png npmPackageName: '@parsifal-m/plugin-permission-backend-module-opa-wrapper' addedDate: '2024-02-26' +status: active diff --git a/microsite/data/plugins/open-dora.yaml b/microsite/data/plugins/open-dora.yaml index 16b4c63969..c3bc4880e3 100644 --- a/microsite/data/plugins/open-dora.yaml +++ b/microsite/data/plugins/open-dora.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/DevoteamNL/opendora/tree/main/backstage-plugin iconUrl: /img/open-dora-icon.png npmPackageName: '@devoteam-nl/open-dora-backstage-plugin' addedDate: '2023-11-29' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/opencost.yaml b/microsite/data/plugins/opencost.yaml index 9e98c85bcb..b8373106f4 100644 --- a/microsite/data/plugins/opencost.yaml +++ b/microsite/data/plugins/opencost.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/opencost.png npmPackageName: '@backstage-community/plugin-opencost' addedDate: '2023-10-26' +status: active diff --git a/microsite/data/plugins/opsgenie.yaml b/microsite/data/plugins/opsgenie.yaml index 8dfe697ff7..daedc8a967 100644 --- a/microsite/data/plugins/opsgenie.yaml +++ b/microsite/data/plugins/opsgenie.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/K-Phoen/backstage-plugin-opsgenie/ iconUrl: https://avatars.githubusercontent.com/u/1818843?s=200&v=4 npmPackageName: '@k-phoen/backstage-plugin-opsgenie' addedDate: '2022-03-13' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/opslevel-maturity.yaml b/microsite/data/plugins/opslevel-maturity.yaml index b9aa00bfb2..5ed3378eb3 100644 --- a/microsite/data/plugins/opslevel-maturity.yaml +++ b/microsite/data/plugins/opslevel-maturity.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/OpsLevel/backstage-plugin iconUrl: https://avatars.githubusercontent.com/u/44910550?s=200&v=4 npmPackageName: backstage-plugin-opslevel-maturity addedDate: '2022-12-07' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/ozone.yaml b/microsite/data/plugins/ozone.yaml index 9093c21ff0..8307ff6f24 100644 --- a/microsite/data/plugins/ozone.yaml +++ b/microsite/data/plugins/ozone.yaml @@ -9,3 +9,5 @@ documentation: https://docs.ozone.one/ozone-end-user-guide/~/changes/7YIrhOgqbpK iconUrl: /img/ozone-logo.png npmPackageName: '@ozonecloud/plugin-ozone' addedDate: '2023-06-14' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/pager-duty.yaml b/microsite/data/plugins/pager-duty.yaml index 448c60f1f7..4d0c42bf20 100644 --- a/microsite/data/plugins/pager-duty.yaml +++ b/microsite/data/plugins/pager-duty.yaml @@ -8,3 +8,4 @@ documentation: https://pagerduty.github.io/backstage-plugin-docs/index.html iconUrl: https://avatars2.githubusercontent.com/u/766800?s=200&v=4 npmPackageName: '@pagerduty/backstage-plugin' addedDate: '2020-12-22' +status: active diff --git a/microsite/data/plugins/parseable-dataset.yaml b/microsite/data/plugins/parseable-dataset.yaml index 743de4e68b..11ab96c50c 100644 --- a/microsite/data/plugins/parseable-dataset.yaml +++ b/microsite/data/plugins/parseable-dataset.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/parseablehq/backstage-plugin/blob/main/parseab iconUrl: https://raw.githubusercontent.com/parseablehq/.github/main/images/logo.svg npmPackageName: '@parseable/backstage-plugin-logstream' addedDate: '2025-07-14' +status: active diff --git a/microsite/data/plugins/periskop.yaml b/microsite/data/plugins/periskop.yaml index 51e9c37103..7865f83a01 100644 --- a/microsite/data/plugins/periskop.yaml +++ b/microsite/data/plugins/periskop.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://raw.githubusercontent.com/periskop-dev/periskop/master/docs/assets/periskop-logo.png npmPackageName: '@backstage-community/plugin-periskop' addedDate: '2022-02-25' +status: active diff --git a/microsite/data/plugins/playlist.yaml b/microsite/data/plugins/playlist.yaml index 2871fb904e..b978ce42b7 100644 --- a/microsite/data/plugins/playlist.yaml +++ b/microsite/data/plugins/playlist.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/playlist-logo.png npmPackageName: '@backstage-community/plugin-playlist' addedDate: '2022-07-02' +status: active diff --git a/microsite/data/plugins/policy-reporter.yaml b/microsite/data/plugins/policy-reporter.yaml index cf8fddac10..b6f1da1833 100644 --- a/microsite/data/plugins/policy-reporter.yaml +++ b/microsite/data/plugins/policy-reporter.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/kyverno/backstage-policy-reporter-plugin iconUrl: https://github.com/cncf/artwork/blob/main/projects/kyverno/icon/color/kyverno-icon-color.png?raw=true npmPackageName: '@kyverno/backstage-plugin-policy-reporter' addedDate: '2025-01-31' +status: active diff --git a/microsite/data/plugins/prometheus.yaml b/microsite/data/plugins/prometheus.yaml index 3160f5046f..45203c6f25 100644 --- a/microsite/data/plugins/prometheus.yaml +++ b/microsite/data/plugins/prometheus.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/prometheus/?utm_source=backst iconUrl: https://avatars.githubusercontent.com/u/3380462?s=200&v=4 npmPackageName: '@roadiehq/backstage-plugin-prometheus' addedDate: '2021-10-06' +status: active diff --git a/microsite/data/plugins/pulumi.yaml b/microsite/data/plugins/pulumi.yaml index 0177b7df0d..547a46c085 100644 --- a/microsite/data/plugins/pulumi.yaml +++ b/microsite/data/plugins/pulumi.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/pulumi/pulumi-backstage-plugin iconUrl: https://www.pulumi.com/logos/brand/avatar-on-white.png npmPackageName: '@pulumi/backstage-plugin-pulumi' addedDate: '2023-09-27' +status: active diff --git a/microsite/data/plugins/puppetdb.yaml b/microsite/data/plugins/puppetdb.yaml index 916d42e8ff..1db8e5eef3 100644 --- a/microsite/data/plugins/puppetdb.yaml +++ b/microsite/data/plugins/puppetdb.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/puppet.png npmPackageName: '@backstage-community/plugin-puppetdb' addedDate: '2023-04-06' +status: active diff --git a/microsite/data/plugins/qeta.yaml b/microsite/data/plugins/qeta.yaml index 0b9dc21cfd..ba3798f5c4 100644 --- a/microsite/data/plugins/qeta.yaml +++ b/microsite/data/plugins/qeta.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/drodil/backstage-plugin-qeta iconUrl: /img/qeta-logo.png npmPackageName: '@drodil/backstage-plugin-qeta' addedDate: '2022-12-21' +status: active diff --git a/microsite/data/plugins/quay.yaml b/microsite/data/plugins/quay.yaml index 9bcbf82de8..abe21e3a77 100644 --- a/microsite/data/plugins/quay.yaml +++ b/microsite/data/plugins/quay.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/quay.svg npmPackageName: '@backstage-community/plugin-quay' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/rafay.yaml b/microsite/data/plugins/rafay.yaml index 7aaf0d7681..5b092af229 100644 --- a/microsite/data/plugins/rafay.yaml +++ b/microsite/data/plugins/rafay.yaml @@ -8,3 +8,5 @@ documentation: https://docs.rafay.co/backstage/setup/ iconUrl: https://avatars.githubusercontent.com/u/33210764?s=200&v=4 npmPackageName: '@rafaysystems/backstage-plugin-rafay' addedDate: '2023-08-24' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/readme.yaml b/microsite/data/plugins/readme.yaml index c914efe355..a316a0c063 100644 --- a/microsite/data/plugins/readme.yaml +++ b/microsite/data/plugins/readme.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main iconUrl: https://raw.githubusercontent.com/AxisCommunications/backstage-plugins/main/plugins/readme/media/readme.png npmPackageName: '@axis-backstage/plugin-readme' addedDate: '2023-12-07' +status: active diff --git a/microsite/data/plugins/relations.yaml b/microsite/data/plugins/relations.yaml index 61ce052453..1601f8c8f5 100644 --- a/microsite/data/plugins/relations.yaml +++ b/microsite/data/plugins/relations.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/relations-backend/docs/pluginIcon.png npmPackageName: '@dweber019/backstage-plugin-relations' addedDate: '2024-05-15' +status: active diff --git a/microsite/data/plugins/renovate-hoster.yaml b/microsite/data/plugins/renovate-hoster.yaml index 76e655b467..1aea5776a8 100644 --- a/microsite/data/plugins/renovate-hoster.yaml +++ b/microsite/data/plugins/renovate-hoster.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/secustor/backstage-plugins/tree/main/plugins/r iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@secustor/backstage-plugin-renovate' addedDate: '2025-03-31' +status: active diff --git a/microsite/data/plugins/revision.yaml b/microsite/data/plugins/revision.yaml index aeb0273602..0f8c994c60 100644 --- a/microsite/data/plugins/revision.yaml +++ b/microsite/data/plugins/revision.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/revision-org/backstage-plugins/tree/main/plugi iconUrl: https://raw.githubusercontent.com/revision-org/revision-assets/b4895ae036e99341576caf1c9bbf92ec89fb65dc/logo-black-full.svg npmPackageName: '@revisionapp/backstage-revision-plugin' addedDate: '2024-01-26' +status: active diff --git a/microsite/data/plugins/rollbar.yaml b/microsite/data/plugins/rollbar.yaml index 3777705b38..5a93122bc5 100644 --- a/microsite/data/plugins/rollbar.yaml +++ b/microsite/data/plugins/rollbar.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://cdn.rollbar.com/wp-content/themes/rollbar/assets/img/logo-white-rollbar.svg npmPackageName: '@backstage-community/plugin-rollbar' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/rootly.yaml b/microsite/data/plugins/rootly.yaml index fa65e2d6ef..38d65fbdab 100644 --- a/microsite/data/plugins/rootly.yaml +++ b/microsite/data/plugins/rootly.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/rootlyhq/backstage-plugin/blob/master/README.m iconUrl: https://raw.githubusercontent.com/rootlyhq/backstage-plugin/master/docs/logo.png npmPackageName: '@rootly/backstage-plugin' addedDate: '2022-06-16' +status: active diff --git a/microsite/data/plugins/rulehub-policy-catalog.yaml b/microsite/data/plugins/rulehub-policy-catalog.yaml new file mode 100644 index 0000000000..3474ebe3db --- /dev/null +++ b/microsite/data/plugins/rulehub-policy-catalog.yaml @@ -0,0 +1,10 @@ +title: RuleHub Policy Catalog +author: Rulehub +authorUrl: https://github.com/rulehub +category: Security +description: Backstage frontend plugin that displays a policy/compliance catalog from a static JSON index. +documentation: https://github.com/rulehub/rulehub-backstage-plugin#readme +iconUrl: https://github.com/rulehub.png +npmPackageName: '@rulehub/rulehub-backstage-plugin' +addedDate: '2026-03-06' +status: active diff --git a/microsite/data/plugins/s3-viewer.yaml b/microsite/data/plugins/s3-viewer.yaml index e454819957..1431fff309 100644 --- a/microsite/data/plugins/s3-viewer.yaml +++ b/microsite/data/plugins/s3-viewer.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/spreadshirt/backstage-plugin-s3/ iconUrl: /img/s3-bucket.svg npmPackageName: '@spreadshirt/backstage-plugin-s3-viewer' addedDate: '2023-01-18' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-datolabs-gcp.yaml b/microsite/data/plugins/scaffolder-backend-datolabs-gcp.yaml index 528506c02d..8bdc5443ba 100644 --- a/microsite/data/plugins/scaffolder-backend-datolabs-gcp.yaml +++ b/microsite/data/plugins/scaffolder-backend-datolabs-gcp.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/datolabs-io/backstage-plugins/blob/main/worksp iconUrl: https://avatars1.githubusercontent.com/u/2810941?s=280&v=4 npmPackageName: '@datolabs/plugin-scaffolder-backend-module-gcp' addedDate: '2024-12-20' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-dotnet.yaml b/microsite/data/plugins/scaffolder-backend-dotnet.yaml index 03fee737cb..eeb9076d36 100644 --- a/microsite/data/plugins/scaffolder-backend-dotnet.yaml +++ b/microsite/data/plugins/scaffolder-backend-dotnet.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/alefcarlos/plusultra-dotnet-backstage-plugins/ iconUrl: /img/scaffolder-backend-dotnet-icon.png npmPackageName: '@plusultra/plugin-scaffolder-dotnet-backend' addedDate: '2022-01-24' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaffolder-backend-git.yaml b/microsite/data/plugins/scaffolder-backend-git.yaml index 4378b36a1e..8c71774bc5 100644 --- a/microsite/data/plugins/scaffolder-backend-git.yaml +++ b/microsite/data/plugins/scaffolder-backend-git.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/arhill05/backstage-plugin-scaffolder-git-actio iconUrl: https://git-scm.com/images/logos/downloads/Git-Logo-2Color.png npmPackageName: '@mdude2314/backstage-plugin-scaffolder-git-actions' addedDate: '2022-05-13' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaffolder-backend-module-rails.yaml b/microsite/data/plugins/scaffolder-backend-module-rails.yaml index f740b95738..b41f9f52d9 100644 --- a/microsite/data/plugins/scaffolder-backend-module-rails.yaml +++ b/microsite/data/plugins/scaffolder-backend-module-rails.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/backstage/blob/master/plugins/scaffo iconUrl: /img/rails-icon.png npmPackageName: '@backstage/plugin-scaffolder-backend-module-rails' addedDate: '2021-06-24' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-module-webex.yaml b/microsite/data/plugins/scaffolder-backend-module-webex.yaml index b0df907966..c13d3eb61c 100644 --- a/microsite/data/plugins/scaffolder-backend-module-webex.yaml +++ b/microsite/data/plugins/scaffolder-backend-module-webex.yaml @@ -3,10 +3,10 @@ title: Cisco Webex Scaffolder Actions author: Robert Lindley authorUrl: https://github.com/Coderrob category: Scaffolder -description: >- - A Backstage.io plugin that enables sending Webex messages via - Incoming Webhooks within scaffolder templates. +description: A Backstage.io plugin that enables sending Webex messages via Incoming Webhooks within scaffolder templates. documentation: https://github.com/Coderrob/backstage-plugin-scaffolder-backend-module-webex -iconUrl: '/img/logo-gradient-on-dark.svg' +iconUrl: /img/logo-gradient-on-dark.svg npmPackageName: '@coderrob/backstage-plugin-scaffolder-backend-module-webex' addedDate: '2024-09-04' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaffolder-backend-ms-teams.yaml b/microsite/data/plugins/scaffolder-backend-ms-teams.yaml index a8f5cd11a2..dd5f745bce 100644 --- a/microsite/data/plugins/scaffolder-backend-ms-teams.yaml +++ b/microsite/data/plugins/scaffolder-backend-ms-teams.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/grvpandey11/backstage-plugin-scaffolder-backen iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@grvpandey11/backstage-plugin-scaffolder-backend-module-ms-teams' addedDate: '2023-08-11' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-odo.yaml b/microsite/data/plugins/scaffolder-backend-odo.yaml index 954d51ecb2..7cf234f920 100644 --- a/microsite/data/plugins/scaffolder-backend-odo.yaml +++ b/microsite/data/plugins/scaffolder-backend-odo.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/redhat-developer/backstage-odo-devfile-plugin/ iconUrl: https://odo.dev/img/logo.png npmPackageName: '@redhat-developer/plugin-scaffolder-odo-actions' addedDate: '2023-12-08' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaffolder-backend-roadie-aws.yaml b/microsite/data/plugins/scaffolder-backend-roadie-aws.yaml index 65808b9af2..2ffb66c518 100644 --- a/microsite/data/plugins/scaffolder-backend-roadie-aws.yaml +++ b/microsite/data/plugins/scaffolder-backend-roadie-aws.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/pl iconUrl: https://upload.wikimedia.org/wikipedia/commons/9/93/Amazon_Web_Services_Logo.svg npmPackageName: '@roadiehq/scaffolder-backend-module-aws' addedDate: '2022-03-07' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml b/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml index e377f4d1f2..9835944617 100644 --- a/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml +++ b/microsite/data/plugins/scaffolder-backend-roadie-http-request.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/pl iconUrl: /img/scaffolder-http-request-logo.svg npmPackageName: '@roadiehq/scaffolder-backend-module-http-request' addedDate: '2022-03-03' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml b/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml index dace41994f..aaa6cff4af 100644 --- a/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml +++ b/microsite/data/plugins/scaffolder-backend-roadie-utils.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/pl iconUrl: /img/scaffolder-utils-logo.png npmPackageName: '@roadiehq/scaffolder-backend-module-utils' addedDate: '2022-03-03' +status: active diff --git a/microsite/data/plugins/scaffolder-backend-slack.yaml b/microsite/data/plugins/scaffolder-backend-slack.yaml index a726751eaf..4f8bf0f15c 100644 --- a/microsite/data/plugins/scaffolder-backend-slack.yaml +++ b/microsite/data/plugins/scaffolder-backend-slack.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/arhill05/backstage-plugin-scaffolder-backend-m iconUrl: /img/Slack-mark-RGB.png npmPackageName: '@mdude2314/backstage-plugin-scaffolder-backend-module-slack' addedDate: '2023-08-04' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaffolder-frontend-devfile-field.yaml b/microsite/data/plugins/scaffolder-frontend-devfile-field.yaml index 3215c63795..73a0d1d472 100644 --- a/microsite/data/plugins/scaffolder-frontend-devfile-field.yaml +++ b/microsite/data/plugins/scaffolder-frontend-devfile-field.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/redhat-developer/backstage-odo-devfile-plugin/ iconUrl: https://landscape.cncf.io/logos/devfile.svg npmPackageName: '@redhat-developer/plugin-scaffolder-frontend-module-devfile-field' addedDate: '2023-12-08' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/scaleops.yaml b/microsite/data/plugins/scaleops.yaml index f11733b35f..cd027e6b53 100644 --- a/microsite/data/plugins/scaleops.yaml +++ b/microsite/data/plugins/scaleops.yaml @@ -7,3 +7,4 @@ documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugin iconUrl: https://avatars.githubusercontent.com/u/100293501?s=200&v=4 npmPackageName: '@terasky/backstage-plugin-scaleops-frontend' addedDate: '2024-12-30' +status: active diff --git a/microsite/data/plugins/scalr.yaml b/microsite/data/plugins/scalr.yaml index 63b2403d39..13cbd09083 100644 --- a/microsite/data/plugins/scalr.yaml +++ b/microsite/data/plugins/scalr.yaml @@ -8,3 +8,4 @@ documentation: 'https://github.com/Scalr/scalr-backstage-plugin/blob/main/README iconUrl: 'https://cdn.prod.website-files.com/63e6a81a3b7fbc072c4af4a7/63eab2d9d2e4cb583a3a7613_Scalr%20Full-Color%20Logo%20Dark.svg' npmPackageName: '@scalr-io/backstage-plugin-scalr' addedDate: '2025-04-15' +status: active diff --git a/microsite/data/plugins/score-card.yaml b/microsite/data/plugins/score-card.yaml index 48f619af2a..68b2908ff8 100644 --- a/microsite/data/plugins/score-card.yaml +++ b/microsite/data/plugins/score-card.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/Oriflame/backstage-plugins/tree/main/plugins/s iconUrl: /img/score-card-plugin-logo.png npmPackageName: '@oriflame/backstage-plugin-score-card' addedDate: '2022-10-06' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/sds_workspace.yaml b/microsite/data/plugins/sds_workspace.yaml index 43d9a0f20e..cbe3c6f832 100644 --- a/microsite/data/plugins/sds_workspace.yaml +++ b/microsite/data/plugins/sds_workspace.yaml @@ -8,3 +8,4 @@ documentation: https://www.npmjs.com/package/@citrixcloud/backstage-sds-workspac iconUrl: /img/citrix_sds.png npmPackageName: '@citrixcloud/backstage-sds-workspaces' addedDate: '2025-10-07' +status: active diff --git a/microsite/data/plugins/search-backend-module-azure-devops-wiki.yaml b/microsite/data/plugins/search-backend-module-azure-devops-wiki.yaml index cc8849ee08..b727aeaed7 100644 --- a/microsite/data/plugins/search-backend-module-azure-devops-wiki.yaml +++ b/microsite/data/plugins/search-backend-module-azure-devops-wiki.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/arhill05/backstage-plugin-search-backend-modul iconUrl: /img/ado-wiki-search-icon.png npmPackageName: '@mdude2314/backstage-plugin-search-backend-module-azure-devops-wiki' addedDate: '2023-06-13' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/search-backend-module-cognitive-search.yaml b/microsite/data/plugins/search-backend-module-cognitive-search.yaml index f8f01720ac..9fbbd61ae5 100644 --- a/microsite/data/plugins/search-backend-module-cognitive-search.yaml +++ b/microsite/data/plugins/search-backend-module-cognitive-search.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/ap-communications/platt-backstage-plugin/tree/ iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@platt/plugin-search-backend-module-cognitive-search' addedDate: '2023-09-13' +status: active diff --git a/microsite/data/plugins/security-insights.yaml b/microsite/data/plugins/security-insights.yaml index fd4a001e14..b0a14ce8c4 100644 --- a/microsite/data/plugins/security-insights.yaml +++ b/microsite/data/plugins/security-insights.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/security-insights/?utm_source iconUrl: https://roadie.io/images/logos/github.png npmPackageName: '@roadiehq/backstage-plugin-security-insights' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/sendgrid.yaml b/microsite/data/plugins/sendgrid.yaml index fbe9baac05..89919d8a44 100644 --- a/microsite/data/plugins/sendgrid.yaml +++ b/microsite/data/plugins/sendgrid.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/CodeVerse-GP/scaffolder-backend-module-sendgri iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@codeverse-gp/scaffolder-backend-module-sendgrid' addedDate: '2025-04-18' +status: active diff --git a/microsite/data/plugins/sentry.yaml b/microsite/data/plugins/sentry.yaml index 06fb9a5d95..6fa36914eb 100644 --- a/microsite/data/plugins/sentry.yaml +++ b/microsite/data/plugins/sentry.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://sentry-brand.storage.googleapis.com/sentry-glyph-white.png npmPackageName: '@backstage-community/plugin-sentry' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/shortcuts.yaml b/microsite/data/plugins/shortcuts.yaml index 7d4e050df0..8be85b69be 100644 --- a/microsite/data/plugins/shortcuts.yaml +++ b/microsite/data/plugins/shortcuts.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/shortcuts.svg npmPackageName: '@backstage-community/plugin-shortcuts' addedDate: '2021-10-06' +status: active diff --git a/microsite/data/plugins/should-i-deploy.yaml b/microsite/data/plugins/should-i-deploy.yaml index 38e77c310f..4c2d8a5823 100644 --- a/microsite/data/plugins/should-i-deploy.yaml +++ b/microsite/data/plugins/should-i-deploy.yaml @@ -6,5 +6,7 @@ category: Humor description: Can I deploy this plugin if it's Friday? Oh, hell no!!! documentation: https://github.com/NandoRFS/backstage-plugin-should-i-deploy/ iconUrl: /img/should-i-deploy-logo.png -npmPackageName: 'backstage-plugin-should-i-deploy' +npmPackageName: backstage-plugin-should-i-deploy addedDate: '2023-11-05' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/signadot.yaml b/microsite/data/plugins/signadot.yaml index 9b6b162901..d981b8390e 100644 --- a/microsite/data/plugins/signadot.yaml +++ b/microsite/data/plugins/signadot.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/signadot/backstage-plugin iconUrl: https://avatars.githubusercontent.com/u/57196635 npmPackageName: '@signadot-lib/backstage-plugin' addedDate: '2025-07-09' +status: active diff --git a/microsite/data/plugins/simple-icons.yaml b/microsite/data/plugins/simple-icons.yaml index 45722ab421..460e0a9a33 100644 --- a/microsite/data/plugins/simple-icons.yaml +++ b/microsite/data/plugins/simple-icons.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/simple-icons/docs/pluginIcon.png npmPackageName: '@dweber019/backstage-plugin-simple-icons' addedDate: '2024-07-30' +status: active diff --git a/microsite/data/plugins/snyk-security.yaml b/microsite/data/plugins/snyk-security.yaml index 8c22d44510..aa94e196f8 100644 --- a/microsite/data/plugins/snyk-security.yaml +++ b/microsite/data/plugins/snyk-security.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/snyk-tech-services/backstage-plugin-snyk/blob/ iconUrl: https://github.com/snyk-tech-services/backstage-plugin-snyk/assets/109112986/95eb1b06-b3e8-4910-9233-11926e02fa18 npmPackageName: 'backstage-plugin-snyk' addedDate: '2021-01-22' +status: active diff --git a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml index 2f17b84f17..b3101402ec 100644 --- a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml +++ b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/solo-io/backstage-plugins-overview#readme iconUrl: /img/solo-io-glooy-circle.png.webp npmPackageName: '@solo.io/dev-portal-backstage-plugin' addedDate: '2023-05-19' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/sonarqube.yaml b/microsite/data/plugins/sonarqube.yaml index 48793adea9..447876a559 100644 --- a/microsite/data/plugins/sonarqube.yaml +++ b/microsite/data/plugins/sonarqube.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/sonarqube-icon.svg npmPackageName: '@backstage-community/plugin-sonarqube' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/spacelift-io.yaml b/microsite/data/plugins/spacelift-io.yaml index fe9b48139f..0f39893c49 100644 --- a/microsite/data/plugins/spacelift-io.yaml +++ b/microsite/data/plugins/spacelift-io.yaml @@ -8,3 +8,4 @@ documentation: https://docs.spacelift.io/integrations/external-integrations/back iconUrl: https://avatars.githubusercontent.com/u/53318513?s=200&v=4 npmPackageName: '@spacelift-io/backstage-integration-frontend' addedDate: '2025-06-10' +status: active diff --git a/microsite/data/plugins/splunk-on-call.yaml b/microsite/data/plugins/splunk-on-call.yaml index 01c114731e..e60653828d 100644 --- a/microsite/data/plugins/splunk-on-call.yaml +++ b/microsite/data/plugins/splunk-on-call.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/splunk-black-white-bg.png npmPackageName: '@backstage-community/plugin-splunk-on-call' addedDate: '2021-11-17' +status: active diff --git a/microsite/data/plugins/stack-overflow.yaml b/microsite/data/plugins/stack-overflow.yaml index 373c08de8f..e48be6e3b9 100644 --- a/microsite/data/plugins/stack-overflow.yaml +++ b/microsite/data/plugins/stack-overflow.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/stack-overflow-logo.svg npmPackageName: '@backstage-community/plugin-stack-overflow' addedDate: '2022-06-14' +status: active diff --git a/microsite/data/plugins/stackoverflow-teams.yaml b/microsite/data/plugins/stackoverflow-teams.yaml index 262a6e9c69..99ebfd1fa6 100644 --- a/microsite/data/plugins/stackoverflow-teams.yaml +++ b/microsite/data/plugins/stackoverflow-teams.yaml @@ -8,3 +8,4 @@ documentation: https://stackoverflowteams.help/en/articles/9692515-backstage-io- iconUrl: /img/stack-overflow-logo.svg npmPackageName: '@stackoverflow/backstage-plugin-stack-overflow-teams' addedDate: '2025-06-10' +status: active diff --git a/microsite/data/plugins/stackstorm.yaml b/microsite/data/plugins/stackstorm.yaml index ae65e56d5d..9eb81f7bc1 100644 --- a/microsite/data/plugins/stackstorm.yaml +++ b/microsite/data/plugins/stackstorm.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/stackstorm.png npmPackageName: '@backstage-community/plugin-stackstorm' addedDate: '2023-02-02' +status: active diff --git a/microsite/data/plugins/statusneo-github-plugin.yaml b/microsite/data/plugins/statusneo-github-plugin.yaml index 6ecc50b4f1..25eb8aea24 100644 --- a/microsite/data/plugins/statusneo-github-plugin.yaml +++ b/microsite/data/plugins/statusneo-github-plugin.yaml @@ -1,3 +1,4 @@ +--- title: Statusneo GitHub Plugin author: Statusneo authorUrl: https://statusneo.com/ @@ -7,3 +8,5 @@ documentation: https://github.com/StatusNeo/backstage-plugin-github iconUrl: /img/github-statusneo.png npmPackageName: '@statusneo/backstage-plugin-github' addedDate: '2023-02-15' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/statuspage.yaml b/microsite/data/plugins/statuspage.yaml index d5402c8ea3..7bc9cc1641 100644 --- a/microsite/data/plugins/statuspage.yaml +++ b/microsite/data/plugins/statuspage.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main iconUrl: https://raw.githubusercontent.com/AxisCommunications/backstage-plugins/main/plugins/statuspage/media/logo.png npmPackageName: '@axis-backstage/plugin-statuspage' addedDate: '2024-02-06' +status: active diff --git a/microsite/data/plugins/synergy.yaml b/microsite/data/plugins/synergy.yaml index 6c5e722322..c47dd28653 100644 --- a/microsite/data/plugins/synergy.yaml +++ b/microsite/data/plugins/synergy.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/jiteshy/backstage-plugin-synergy/blob/main/REA iconUrl: /img/synergy-logo.png npmPackageName: '@jiteshy/backstage-plugin-synergy' addedDate: '2024-12-03' +status: active diff --git a/microsite/data/plugins/sysdig.yaml b/microsite/data/plugins/sysdig.yaml index 220f4468b2..ab1bfb92bb 100644 --- a/microsite/data/plugins/sysdig.yaml +++ b/microsite/data/plugins/sysdig.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/sysdiglabs/backstage-plugin-sysdig/blob/main/R iconUrl: https://sysdig.com/wp-content/uploads/sysdig-logo-new-white.svg npmPackageName: '@sysdig/backstage-plugin-sysdig' addedDate: '2024-03-06' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/targetboard.yaml b/microsite/data/plugins/targetboard.yaml index 2cac367eec..0077fbbbbe 100644 --- a/microsite/data/plugins/targetboard.yaml +++ b/microsite/data/plugins/targetboard.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/targetboard/backstage-plugin/blob/main/README. iconUrl: https://app.targetboard.ai/assets/TargetBoard-Backstage-logo.svg npmPackageName: '@targetboard/backstage-plugin' addedDate: '2025-11-02' +status: active diff --git a/microsite/data/plugins/tech-insights.yaml b/microsite/data/plugins/tech-insights.yaml index f1697467b0..971f72e096 100644 --- a/microsite/data/plugins/tech-insights.yaml +++ b/microsite/data/plugins/tech-insights.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/tech-insights/ iconUrl: https://roadie.io/images/logos/tech-insights.png npmPackageName: '@backstage-community/plugin-tech-insights' addedDate: '2022-03-31' +status: active diff --git a/microsite/data/plugins/tech-radar.yaml b/microsite/data/plugins/tech-radar.yaml index 5db68eef27..35c7432931 100644 --- a/microsite/data/plugins/tech-radar.yaml +++ b/microsite/data/plugins/tech-radar.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/tech-radar.svg npmPackageName: '@backstage-community/plugin-tech-radar' addedDate: '2020-11-03' +status: active diff --git a/microsite/data/plugins/tekton-pipelines.yaml b/microsite/data/plugins/tekton-pipelines.yaml index fe19ab2af2..d29e651ee1 100644 --- a/microsite/data/plugins/tekton-pipelines.yaml +++ b/microsite/data/plugins/tekton-pipelines.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/jquad-group/backstage-jquad#readme iconUrl: https://raw.githubusercontent.com/jquad-group/backstage-jquad/main/img/tekton-horizontal-color.png npmPackageName: '@jquad-group/plugin-tekton-pipelines' addedDate: '2022-08-08' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/tekton.yaml b/microsite/data/plugins/tekton.yaml index 71aa433a83..346d472a47 100644 --- a/microsite/data/plugins/tekton.yaml +++ b/microsite/data/plugins/tekton.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/tekton.svg npmPackageName: '@backstage-community/plugin-tekton' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/template-designer.yaml b/microsite/data/plugins/template-designer.yaml index cf1e2ed7aa..9146da018a 100644 --- a/microsite/data/plugins/template-designer.yaml +++ b/microsite/data/plugins/template-designer.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/tduniec/template-designer-plugin/blob/main/REA iconUrl: https://raw.githubusercontent.com/tduniec/template-designer-plugin/main/img/logo/templateDesignerLogo.png npmPackageName: '@tduniec/plugin-template-designer' addedDate: '2025-11-19' +status: active diff --git a/microsite/data/plugins/time-saver.yaml b/microsite/data/plugins/time-saver.yaml index 6637eddb44..cac5f3f285 100644 --- a/microsite/data/plugins/time-saver.yaml +++ b/microsite/data/plugins/time-saver.yaml @@ -8,3 +8,5 @@ documentation: https://github.com/tduniec/backstage-timesaver-plugin/blob/main/R iconUrl: https://raw.githubusercontent.com/tduniec/backstage-timesaver-plugin/main/img/tsLogo.png npmPackageName: '@tduniec/backstage-plugin-time-saver' addedDate: '2024-01-30' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/tips.yaml b/microsite/data/plugins/tips.yaml index 03327f3e41..7650c9d868 100644 --- a/microsite/data/plugins/tips.yaml +++ b/microsite/data/plugins/tips.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/ iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/tips/docs/pluginIcon.png npmPackageName: '@dweber019/backstage-plugin-tips' addedDate: '2023-10-08' +status: active diff --git a/microsite/data/plugins/todo.yaml b/microsite/data/plugins/todo.yaml index ab353e8150..dc6366b6de 100644 --- a/microsite/data/plugins/todo.yaml +++ b/microsite/data/plugins/todo.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: https://backstage.io/img/todo-logo.png npmPackageName: '@backstage-community/plugin-todo' addedDate: '2021-03-16' +status: active diff --git a/microsite/data/plugins/toolbox.yaml b/microsite/data/plugins/toolbox.yaml index b488b24369..b115b56759 100644 --- a/microsite/data/plugins/toolbox.yaml +++ b/microsite/data/plugins/toolbox.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/drodil/backstage-plugin-toolbox iconUrl: /img/toolbox-logo.png npmPackageName: '@drodil/backstage-plugin-toolbox' addedDate: '2023-01-09' +status: active diff --git a/microsite/data/plugins/topology.yaml b/microsite/data/plugins/topology.yaml index f1b74ef45b..724ee9c0b9 100644 --- a/microsite/data/plugins/topology.yaml +++ b/microsite/data/plugins/topology.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/topology.svg npmPackageName: '@backstage-community/plugin-topology' addedDate: '2023-05-15' +status: active diff --git a/microsite/data/plugins/torque.yaml b/microsite/data/plugins/torque.yaml index a469c455fc..705ab72dc4 100644 --- a/microsite/data/plugins/torque.yaml +++ b/microsite/data/plugins/torque.yaml @@ -1,7 +1,7 @@ --- title: Torque author: Quali -authorUrl: 'https://www.qtorque.io/' +authorUrl: https://www.qtorque.io/ category: Orchestration description: | Deploy the environments you need to keep building, testing and delivering incredible code. @@ -10,3 +10,5 @@ documentation: https://github.com/QualiTorque/torque-backstage-plugin/tree/main/ iconUrl: https://user-images.githubusercontent.com/8643801/214640977-751bc338-6b77-40a0-a897-cba41b6f006a.png npmPackageName: '@qtorque/backstage-torque-plugin' addedDate: '2023-01-25' +status: inactive +staleSince: '2026-02-17' diff --git a/microsite/data/plugins/travis-ci.yaml b/microsite/data/plugins/travis-ci.yaml index a5cec13d22..7adf3565e8 100644 --- a/microsite/data/plugins/travis-ci.yaml +++ b/microsite/data/plugins/travis-ci.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/travis-ci/?utm_source=backsta iconUrl: https://roadie.io/images/logos/travis.png npmPackageName: '@roadiehq/backstage-plugin-travis-ci' addedDate: '2021-04-20' +status: active diff --git a/microsite/data/plugins/umami.yaml b/microsite/data/plugins/umami.yaml index 5f1fc208d4..0a3559d710 100644 --- a/microsite/data/plugins/umami.yaml +++ b/microsite/data/plugins/umami.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/AxisCommunications/backstage-plugins/blob/main iconUrl: /img/umami-logo.svg npmPackageName: '@axis-backstage/plugin-analytics-module-umami' addedDate: '2024-02-05' +status: active diff --git a/microsite/data/plugins/usage-statistics.yaml b/microsite/data/plugins/usage-statistics.yaml index a6722f7dd9..44d6ddb5f4 100644 --- a/microsite/data/plugins/usage-statistics.yaml +++ b/microsite/data/plugins/usage-statistics.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/CodeVerse-GP/usage-statistics/blob/main/README iconUrl: '/img/logo-gradient-on-dark.svg' npmPackageName: '@codeverse-gp/plugin-usage-statistics' addedDate: '2025-11-05' +status: active diff --git a/microsite/data/plugins/vault.yaml b/microsite/data/plugins/vault.yaml index cec13cc63b..30a5f5f227 100644 --- a/microsite/data/plugins/vault.yaml +++ b/microsite/data/plugins/vault.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/tree/main/workspac iconUrl: /img/vault.png npmPackageName: '@backstage-community/plugin-vault' addedDate: '2022-06-03' +status: active diff --git a/microsite/data/plugins/wheel-of-names.yaml b/microsite/data/plugins/wheel-of-names.yaml index 0377742ab0..6376d41c59 100644 --- a/microsite/data/plugins/wheel-of-names.yaml +++ b/microsite/data/plugins/wheel-of-names.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/backstage/community-plugins/blob/main/workspac iconUrl: /img/wheel-of-names.svg npmPackageName: '@backstage-community/plugin-wheel-of-names' addedDate: '2025-05-21' +status: active diff --git a/microsite/data/plugins/wiz.yaml b/microsite/data/plugins/wiz.yaml index 1496231dfa..672e72d3f7 100644 --- a/microsite/data/plugins/wiz.yaml +++ b/microsite/data/plugins/wiz.yaml @@ -8,3 +8,4 @@ documentation: https://roadie.io/backstage/plugins/wiz/ iconUrl: https://roadie.io/images/wiz-logo.png npmPackageName: '@roadiehq/backstage-plugin-wiz' addedDate: '2024-10-14' +status: active diff --git a/microsite/data/plugins/xcmetrics.yaml b/microsite/data/plugins/xcmetrics.yaml index 09a8b1547f..dde1b88ea4 100644 --- a/microsite/data/plugins/xcmetrics.yaml +++ b/microsite/data/plugins/xcmetrics.yaml @@ -8,3 +8,4 @@ documentation: https://xcmetrics.io/ iconUrl: /img/xcmetrics-icon.png npmPackageName: '@backstage-community/plugin-xcmetrics' addedDate: '2021-08-06' +status: active diff --git a/microsite/data/plugins/xkcd.yaml b/microsite/data/plugins/xkcd.yaml index 370718a08f..e01e277bd4 100644 --- a/microsite/data/plugins/xkcd.yaml +++ b/microsite/data/plugins/xkcd.yaml @@ -8,3 +8,4 @@ documentation: https://github.com/Vity01/backstage-xkcd/ iconUrl: https://raw.githubusercontent.com/Vity01/backstage-xkcd/main/docs/xkcd.svg npmPackageName: 'backstage-plugin-xkcd' addedDate: '2023-05-05' +status: active diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index ce75a2e063..718a337b01 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -450,6 +450,13 @@ const config: Config = { ], }, image: 'img/sharing-opengraph.png', + metadata: [ + { + name: 'description', + content: + 'Backstage is an open source developer portal framework that centralizes your software catalog, unifies infrastructure tools, and helps teams ship high-quality code faster.', + }, + ], footer: { links: [ { diff --git a/microsite/package.json b/microsite/package.json index 0da6c2c8be..51e1284668 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -20,8 +20,8 @@ }, "prettier": "@backstage/cli/config/prettier", "resolutions": { - "@swc/core": "1.15.3", - "@swc/html": "1.15.3", + "@swc/core": "1.15.18", + "@swc/html": "1.15.18", "node-polyfill-webpack-plugin": "^3.0.0" }, "dependencies": { diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index b3053e3953..ec4585b407 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -69,8 +69,16 @@ export default { ]), sidebarElementWithIndex({ label: 'Using Backstage' }, [ 'getting-started/logging-in', - 'getting-started/register-a-component', - 'getting-started/create-a-component', + 'getting-started/viewing-catalog', + 'getting-started/view-what-you-own', + 'getting-started/viewing-entity-relationships', + 'getting-started/filter-catalog', + sidebarElementWithIndex({ label: 'Managing Components' }, [ + 'getting-started/register-a-component', + 'getting-started/create-a-component', + 'getting-started/update-a-component', + 'getting-started/unregister-delete-component', + ]), ]), 'overview/support', 'getting-started/keeping-backstage-updated', @@ -105,6 +113,14 @@ export default { description: 'Features powering the core of Backstage.', }, [ + sidebarElementWithIndex( + { + label: 'AI', + description: + 'Features in Backstage you can leverage with your AI tools.', + }, + ['ai/mcp-actions', 'ai/well-known-actions'], + ), sidebarElementWithIndex( { label: 'Auth and Identity', @@ -391,6 +407,7 @@ export default { 'integrations/google-cloud-storage/locations', ]), sidebarElementWithIndex({ label: 'LDAP' }, ['integrations/ldap/org']), + sidebarElementWithIndex({ label: 'Okta' }, ['integrations/okta/org']), ], ), sidebarElementWithIndex( @@ -438,7 +455,11 @@ export default { ), sidebarElementWithIndex( { label: 'Publishing', description: 'Publishing your plugins.' }, - ['plugins/publish-private', 'plugins/add-to-directory'], + [ + 'plugins/publish-private', + 'plugins/add-to-directory', + 'plugins/plugin-directory-audit', + ], ), 'plugins/observability', ], @@ -517,6 +538,7 @@ export default { 'backend-system/core-services/identity', 'backend-system/core-services/lifecycle', 'backend-system/core-services/logger', + 'backend-system/core-services/metrics', 'backend-system/core-services/permissions', 'backend-system/core-services/permissions-registry', 'backend-system/core-services/plugin-metadata', @@ -538,15 +560,15 @@ export default { ), sidebarElementWithIndex( { - label: 'New Frontend System', - description: 'New frontend system components and architecture.', + label: 'Frontend System', + description: 'Frontend system components and architecture.', }, [ 'frontend-system/index', sidebarElementWithIndex( { label: 'Architecture', - description: 'Architecture of the new frontend system.', + description: 'Architecture of the frontend system.', differentiator: 'frontend-system/', }, [ @@ -556,6 +578,7 @@ export default { 'frontend-system/architecture/extensions', 'frontend-system/architecture/extension-blueprints', 'frontend-system/architecture/extension-overrides', + 'frontend-system/architecture/sharing-extensions', 'frontend-system/architecture/references', 'frontend-system/architecture/utility-apis', 'frontend-system/architecture/routes', @@ -583,6 +606,7 @@ export default { }, [ 'frontend-system/building-apps/index', + 'frontend-system/building-apps/installing-plugins', 'frontend-system/building-apps/configuring-extensions', 'frontend-system/building-apps/built-in-extensions', 'frontend-system/building-apps/plugin-conversion', diff --git a/microsite/src/pages/home/_home.tsx b/microsite/src/pages/home/_home.tsx index d4e66b6729..eded6a7af2 100644 --- a/microsite/src/pages/home/_home.tsx +++ b/microsite/src/pages/home/_home.tsx @@ -84,7 +84,7 @@ const HomePage = () => { label: 'GITHUB', }, { - link: 'https://info.backstage.spotify.com/office-hours', + link: 'https://spoti.fi/backstageofficehours', label: 'OFFICE HOURS', }, ]} diff --git a/microsite/src/pages/plugins/_pluginCard.tsx b/microsite/src/pages/plugins/_pluginCard.tsx index ba7864f9aa..a464dd2fc6 100644 --- a/microsite/src/pages/plugins/_pluginCard.tsx +++ b/microsite/src/pages/plugins/_pluginCard.tsx @@ -14,6 +14,7 @@ export interface IPluginData { addedDate: string; isNew: boolean; order?: number; + status: 'active' | 'inactive' | 'archived'; } const defaultIconUrl = '/img/logo-gradient-on-dark.svg'; diff --git a/microsite/src/pages/plugins/index.tsx b/microsite/src/pages/plugins/index.tsx index 1a87f9c0af..9acf3fb341 100644 --- a/microsite/src/pages/plugins/index.tsx +++ b/microsite/src/pages/plugins/index.tsx @@ -117,12 +117,24 @@ const Plugins = () => { const corePlugins = useMemo(() => { return plugins.corePlugins + .filter(pluginData => pluginData.status !== 'archived') .filter(pluginData => matchesCategory(pluginData, selectedCategories)) .filter(pluginData => matchesSearch(pluginData, searchTerm)); }, [selectedCategories, searchTerm]); const otherPlugins = useMemo(() => { return plugins.otherPlugins + .filter( + pluginData => + pluginData.status !== 'inactive' && pluginData.status !== 'archived', + ) + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + + const inactivePlugins = useMemo(() => { + return plugins.otherPlugins + .filter(pluginData => pluginData.status === 'inactive') .filter(pluginData => matchesCategory(pluginData, selectedCategories)) .filter(pluginData => matchesSearch(pluginData, searchTerm)); }, [selectedCategories, searchTerm]); @@ -162,15 +174,17 @@ const Plugins = () => { /> - {corePlugins.length === 0 && otherPlugins.length === 0 && ( -
    -

    No plugins found

    -

    - We couldn't find any plugins matching your criteria. Please try - adjusting your search or filter settings. -

    -
    - )} + {corePlugins.length === 0 && + otherPlugins.length === 0 && + inactivePlugins.length === 0 && ( +
    +

    No plugins found

    +

    + We couldn't find any plugins matching your criteria. Please try + adjusting your search or filter settings. +

    +
    + )} {showCoreFeatures && corePlugins.length > 0 && (
    @@ -185,7 +199,7 @@ const Plugins = () => { {showOtherPlugins && otherPlugins.length > 0 && (
    -

    All Plugins ({otherPlugins.length})

    +

    Active Plugins ({otherPlugins.length})

    Friendly reminder: While we love the variety and contributions of our open source plugins, they haven't been fully vetted by the @@ -199,6 +213,36 @@ const Plugins = () => {

    )} + + {showOtherPlugins && + otherPlugins.length === 0 && + inactivePlugins.length > 0 && ( +
    +

    Active Plugins (0)

    +

    We couldn't find any active plugins matching your criteria.

    +
    + )} + + {inactivePlugins.length > 0 && ( +
    +

    Inactive Plugins ({inactivePlugins.length})

    +

    + These plugins are no longer actively maintained as their NPM + package has not seen an update in more than 365 days. They are + kept here for reference but may not work with current versions of + Backstage. Details on the audit process can be found in the{' '} + + Plugin Directory Audit + {' '} + documentation. +

    +
    + {inactivePlugins.map(pluginData => ( + + ))} +
    +
    + )} ); diff --git a/microsite/static/img/flagsmith.svg b/microsite/static/img/flagsmith.svg new file mode 100644 index 0000000000..d8039477ca --- /dev/null +++ b/microsite/static/img/flagsmith.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 9a1c627f7e..0935b07949 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -4053,90 +4053,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-darwin-arm64@npm:1.15.3" +"@swc/core-darwin-arm64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-darwin-arm64@npm:1.15.18" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-darwin-x64@npm:1.15.3" +"@swc/core-darwin-x64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-darwin-x64@npm:1.15.18" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.3" +"@swc/core-linux-arm-gnueabihf@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.18" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-linux-arm64-gnu@npm:1.15.3" +"@swc/core-linux-arm64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-linux-arm64-musl@npm:1.15.3" +"@swc/core-linux-arm64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-linux-x64-gnu@npm:1.15.3" +"@swc/core-linux-x64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-linux-x64-musl@npm:1.15.3" +"@swc/core-linux-x64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-x64-musl@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-win32-arm64-msvc@npm:1.15.3" +"@swc/core-win32-arm64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.18" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-win32-ia32-msvc@npm:1.15.3" +"@swc/core-win32-ia32-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.18" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core-win32-x64-msvc@npm:1.15.3" +"@swc/core-win32-x64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.18" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/core@npm:1.15.3" +"@swc/core@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core@npm:1.15.18" dependencies: - "@swc/core-darwin-arm64": "npm:1.15.3" - "@swc/core-darwin-x64": "npm:1.15.3" - "@swc/core-linux-arm-gnueabihf": "npm:1.15.3" - "@swc/core-linux-arm64-gnu": "npm:1.15.3" - "@swc/core-linux-arm64-musl": "npm:1.15.3" - "@swc/core-linux-x64-gnu": "npm:1.15.3" - "@swc/core-linux-x64-musl": "npm:1.15.3" - "@swc/core-win32-arm64-msvc": "npm:1.15.3" - "@swc/core-win32-ia32-msvc": "npm:1.15.3" - "@swc/core-win32-x64-msvc": "npm:1.15.3" + "@swc/core-darwin-arm64": "npm:1.15.18" + "@swc/core-darwin-x64": "npm:1.15.18" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.18" + "@swc/core-linux-arm64-gnu": "npm:1.15.18" + "@swc/core-linux-arm64-musl": "npm:1.15.18" + "@swc/core-linux-x64-gnu": "npm:1.15.18" + "@swc/core-linux-x64-musl": "npm:1.15.18" + "@swc/core-win32-arm64-msvc": "npm:1.15.18" + "@swc/core-win32-ia32-msvc": "npm:1.15.18" + "@swc/core-win32-x64-msvc": "npm:1.15.18" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.25" peerDependencies: @@ -4165,7 +4165,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/280330d82328818138ed64fdcf9ea9abde6b6f16eca65a9d4db27dde06a8dfffd2649f3447d2243387277513c7430fa4142cafcfd64e943d682ce6a713cb8c2d + checksum: 10/ef198b9feb6eee034e3a912c37988ece2885fec35419e8245d467adbc1fc47a5c3e61869d1bdbe6fff76cbd9186ef278120cbb9746d5f7446576f4a7f15c2dcd languageName: node linkType: hard @@ -4176,91 +4176,91 @@ __metadata: languageName: node linkType: hard -"@swc/html-darwin-arm64@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-darwin-arm64@npm:1.15.3" +"@swc/html-darwin-arm64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-darwin-arm64@npm:1.15.18" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/html-darwin-x64@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-darwin-x64@npm:1.15.3" +"@swc/html-darwin-x64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-darwin-x64@npm:1.15.18" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/html-linux-arm-gnueabihf@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-linux-arm-gnueabihf@npm:1.15.3" +"@swc/html-linux-arm-gnueabihf@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-linux-arm-gnueabihf@npm:1.15.18" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/html-linux-arm64-gnu@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-linux-arm64-gnu@npm:1.15.3" +"@swc/html-linux-arm64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-linux-arm64-gnu@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/html-linux-arm64-musl@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-linux-arm64-musl@npm:1.15.3" +"@swc/html-linux-arm64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-linux-arm64-musl@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/html-linux-x64-gnu@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-linux-x64-gnu@npm:1.15.3" +"@swc/html-linux-x64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-linux-x64-gnu@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/html-linux-x64-musl@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-linux-x64-musl@npm:1.15.3" +"@swc/html-linux-x64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-linux-x64-musl@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/html-win32-arm64-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-win32-arm64-msvc@npm:1.15.3" +"@swc/html-win32-arm64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-win32-arm64-msvc@npm:1.15.18" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/html-win32-ia32-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-win32-ia32-msvc@npm:1.15.3" +"@swc/html-win32-ia32-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-win32-ia32-msvc@npm:1.15.18" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/html-win32-x64-msvc@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html-win32-x64-msvc@npm:1.15.3" +"@swc/html-win32-x64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html-win32-x64-msvc@npm:1.15.18" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/html@npm:1.15.3": - version: 1.15.3 - resolution: "@swc/html@npm:1.15.3" +"@swc/html@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/html@npm:1.15.18" dependencies: "@swc/counter": "npm:^0.1.3" - "@swc/html-darwin-arm64": "npm:1.15.3" - "@swc/html-darwin-x64": "npm:1.15.3" - "@swc/html-linux-arm-gnueabihf": "npm:1.15.3" - "@swc/html-linux-arm64-gnu": "npm:1.15.3" - "@swc/html-linux-arm64-musl": "npm:1.15.3" - "@swc/html-linux-x64-gnu": "npm:1.15.3" - "@swc/html-linux-x64-musl": "npm:1.15.3" - "@swc/html-win32-arm64-msvc": "npm:1.15.3" - "@swc/html-win32-ia32-msvc": "npm:1.15.3" - "@swc/html-win32-x64-msvc": "npm:1.15.3" + "@swc/html-darwin-arm64": "npm:1.15.18" + "@swc/html-darwin-x64": "npm:1.15.18" + "@swc/html-linux-arm-gnueabihf": "npm:1.15.18" + "@swc/html-linux-arm64-gnu": "npm:1.15.18" + "@swc/html-linux-arm64-musl": "npm:1.15.18" + "@swc/html-linux-x64-gnu": "npm:1.15.18" + "@swc/html-linux-x64-musl": "npm:1.15.18" + "@swc/html-win32-arm64-msvc": "npm:1.15.18" + "@swc/html-win32-ia32-msvc": "npm:1.15.18" + "@swc/html-win32-x64-msvc": "npm:1.15.18" dependenciesMeta: "@swc/html-darwin-arm64": optional: true @@ -4282,7 +4282,7 @@ __metadata: optional: true "@swc/html-win32-x64-msvc": optional: true - checksum: 10/753ddec5a5fb53a5245b4e5ab990c8dc9e6aff2c3da99ab671be2535d3fc566953dac4aa30c9a435e78a80901384fbc8f43c603c379abe2d21a11303dd804393 + checksum: 10/5fdef26fa88693c2913d2ee147332ab366bc6d41726ea2b57db31d3992fbf47aa5b881537f32a077535a64f896a7f7af8f0092343d74256fd8c290431e1a8407 languageName: node linkType: hard @@ -4311,13 +4311,6 @@ __metadata: languageName: node linkType: hard -"@trysound/sax@npm:0.2.0": - version: 0.2.0 - resolution: "@trysound/sax@npm:0.2.0" - checksum: 10/7379713eca480ac0d9b6c7b063e06b00a7eac57092354556c81027066eb65b61ea141a69d0cc2e15d32e05b2834d4c9c2184793a5e36bbf5daf05ee5676af18c - languageName: node - linkType: hard - "@tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -5081,14 +5074,14 @@ __metadata: linkType: hard "ajv@npm:^6.12.5": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" + version: 6.14.0 + resolution: "ajv@npm:6.14.0" dependencies: fast-deep-equal: "npm:^3.1.1" fast-json-stable-stringify: "npm:^2.0.0" json-schema-traverse: "npm:^0.4.1" uri-js: "npm:^4.2.2" - checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c + checksum: 10/c71f14dd2b6f2535d043f74019c8169f7aeb1106bafbb741af96f34fdbf932255c919ddd46344043d03b62ea0ccb319f83667ec5eedf612393f29054fe5ce4a5 languageName: node linkType: hard @@ -8555,9 +8548,9 @@ __metadata: linkType: hard "immutable@npm:^5.0.2": - version: 5.0.2 - resolution: "immutable@npm:5.0.2" - checksum: 10/89b1117c610024b7a9214eade8b9f1ed38b00c82235f119515cfa5eaf26270eccbc803296d4c3c12f53e50802f042f84d811998910b866363913720da768472e + version: 5.1.5 + resolution: "immutable@npm:5.1.5" + checksum: 10/7aec2740239772ec8e92e793c991bd809203a97694f4ff3a18e50e28f9a6b02393ad033d87b458037bdf8c0ea37d4446d640e825f6171df3405cf6cf300ce028 languageName: node linkType: hard @@ -12493,11 +12486,11 @@ __metadata: linkType: hard "qs@npm:^6.12.3, qs@npm:~6.14.0": - version: 6.14.1 - resolution: "qs@npm:6.14.1" + version: 6.14.2 + resolution: "qs@npm:6.14.2" dependencies: side-channel: "npm:^1.1.0" - checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5 + checksum: 10/682933a85bb4b7bd0d66e13c0a40d9e612b5e4bcc2cb9238f711a9368cd22d91654097a74fff93551e58146db282c56ac094957dfdc60ce64ea72c3c9d7779ac languageName: node linkType: hard @@ -13312,10 +13305,10 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4": - version: 1.2.4 - resolution: "sax@npm:1.2.4" - checksum: 10/09b79ff6dc09689a24323352117c94593c69db348997b2af0edbd82fa08aba47d778055bf9616b57285bb73d25d790900c044bf631a8f10c8252412e3f3fe5dd +"sax@npm:^1.2.4, sax@npm:^1.5.0": + version: 1.5.0 + resolution: "sax@npm:1.5.0" + checksum: 10/9012ff37dda7a7ac5da45db2143b04036103e8bef8d586c3023afd5df6caf0ebd7f38017eee344ad2e2247eded7d38e9c42cf291d8dd91781352900ac0fd2d9f languageName: node linkType: hard @@ -14132,19 +14125,19 @@ __metadata: linkType: hard "svgo@npm:^3.0.2, svgo@npm:^3.2.0": - version: 3.3.2 - resolution: "svgo@npm:3.3.2" + version: 3.3.3 + resolution: "svgo@npm:3.3.3" dependencies: - "@trysound/sax": "npm:0.2.0" commander: "npm:^7.2.0" css-select: "npm:^5.1.0" css-tree: "npm:^2.3.1" css-what: "npm:^6.1.0" csso: "npm:^5.0.5" picocolors: "npm:^1.0.0" + sax: "npm:^1.5.0" bin: svgo: ./bin/svgo - checksum: 10/82fdea9b938884d808506104228e4d3af0050d643d5b46ff7abc903ff47a91bbf6561373394868aaf07a28f006c4057b8fbf14bbd666298abdd7cc590d4f7700 + checksum: 10/f3c1b4d05d1704483e53515d5995af5f06a2718df85e3a8320f57bb256b8dc926b84c87a1a9b98e9d3ca1224314cc0676a803bdd03163508292f2d45c7077096 languageName: node linkType: hard diff --git a/mkdocs.yml b/mkdocs.yml index c07ff5afc4..618f77a59f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ plugins: nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' + - Technical overview: 'overview/technical-overview.md' - Architecture overview: 'overview/architecture-overview.md' - Project Roadmap: 'overview/roadmap.md' - Vision: 'overview/vision.md' @@ -37,8 +38,15 @@ nav: - Kubernetes: 'deployment/k8s.md' - Using Backstage: - Logging in: 'getting-started/logging-in.md' - - Register a component: 'getting-started/register-a-component.md' - - Create a component: 'getting-started/create-a-component.md' + - Viewing the Catalog: 'getting-started/viewing-catalog.md' + - Viewing what you own: 'getting-started/view-what-you-own.md' + - Viewing entity relationships: 'getting-started/viewing-entity-relationships.md' + - Filtering the Catalog: 'getting-started/filter-catalog.md' + - Managing Components: + - Register a component: 'getting-started/register-a-component.md' + - Create a component: 'getting-started/create-a-component.md' + - Update a component: 'getting-started/update-a-component.md' + - Unregister/delete a component: 'getting-started/unregister-delete-component.md' - Keeping Backstage Updated: 'getting-started/keeping-backstage-updated.md' - Core Features: - Software Catalog: @@ -142,6 +150,7 @@ nav: - Composability System: 'plugins/composability.md' - 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' diff --git a/package.json b/package.json index d03937d567..c89dcefce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.48.0-next.2", + "version": "1.49.0-next.2", "backstage": { "cli": { "new": { @@ -16,15 +16,11 @@ "type": "git", "url": "https://github.com/backstage/backstage" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "scripts": { - "build-storybook": "storybook build --output-dir dist-storybook", - "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "build:all": "backstage-cli repo build --all", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", @@ -32,6 +28,8 @@ "build:backend": "yarn workspace example-backend build", "build:knip-reports": "backstage-repo-tools knip-reports", "build:plugins-report": "node ./scripts/build-plugins-report", + "build-storybook": "storybook build --output-dir dist-storybook", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "clean": "backstage-cli repo clean", "create-plugin": "echo \"use 'yarn new' instead\"", "dev": "echo \"use 'yarn start' instead\"", @@ -54,10 +52,10 @@ "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", "snyk:test:package": "yarn snyk:test --include", "start": "backstage-cli repo start", - "start-backend": "echo \"Use 'yarn start example-backend' instead\"", "start:docker": "docker compose -f docker-compose.deps.yml up --wait && BACKSTAGE_ENV=docker yarn start", "start:legacy": "yarn start example-app-legacy example-backend", "start:microsite": "cd microsite/ && yarn start", + "start-backend": "echo \"Use 'yarn start example-backend' instead\"", "storybook": "storybook dev -p 6006", "sync-issue-templates": "node ./.github/ISSUE_TEMPLATE/sync.js", "techdocs-cli": "node scripts/techdocs-cli.js", @@ -127,6 +125,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:*", + "@backstage/cli-defaults": "workspace:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", "@backstage/e2e-test-utils": "workspace:*", @@ -140,7 +139,7 @@ "@storybook/addon-a11y": "^10.3.0-alpha.1", "@storybook/addon-docs": "^10.3.0-alpha.1", "@storybook/addon-links": "^10.3.0-alpha.1", - "@storybook/addon-mcp": "^0.2.2", + "@storybook/addon-mcp": "^0.3.0", "@storybook/addon-themes": "^10.3.0-alpha.1", "@storybook/addon-vitest": "^10.3.0-alpha.1", "@storybook/react-vite": "^10.3.0-alpha.1", @@ -165,7 +164,7 @@ "jest": "^30", "js-yaml": "^4.1.1", "jsdom": "^27", - "lint-staged": "^15.0.0", + "lint-staged": "^16.0.0", "madge": "^8.0.0", "minimist": "^1.2.5", "node-gyp": "^10.0.0", @@ -173,7 +172,7 @@ "semver": "^7.5.3", "shx": "^0.4.0", "sloc": "^0.3.1", - "sort-package-json": "^2.8.0", + "sort-package-json": "^3.0.0", "storybook": "^10.3.0-alpha.1", "ts-morph": "^24.0.0", "typedoc": "^0.28.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 2063f62327..e7bfdf0c89 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/app-defaults +## 1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 1.7.5 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + ## 1.7.5-next.2 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 7ce485c5d1..b35830dbd1 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.7.5-next.2", + "version": "1.7.6-next.0", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-example-plugin/CHANGELOG.md b/packages/app-example-plugin/CHANGELOG.md index 7cbb2eec34..c345e22916 100644 --- a/packages/app-example-plugin/CHANGELOG.md +++ b/packages/app-example-plugin/CHANGELOG.md @@ -1,5 +1,29 @@ # app-example-plugin +## 0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + +## 0.0.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + ## 0.0.32-next.0 ### Patch Changes diff --git a/packages/app-example-plugin/package.json b/packages/app-example-plugin/package.json index f9e8c9152e..40c67c90f1 100644 --- a/packages/app-example-plugin/package.json +++ b/packages/app-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-example-plugin", - "version": "0.0.32-next.0", + "version": "0.0.33-next.1", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-example-plugin/report.api.md b/packages/app-example-plugin/report.api.md index 028795ed72..432174da68 100644 --- a/packages/app-example-plugin/report.api.md +++ b/packages/app-example-plugin/report.api.md @@ -4,7 +4,10 @@ ```ts import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSX as JSX_3 } from 'react/jsx-runtime'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -21,26 +24,75 @@ const examplePlugin: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; } diff --git a/packages/app-legacy/CHANGELOG.md b/packages/app-legacy/CHANGELOG.md index f73b970d1e..921b8c5d2f 100644 --- a/packages/app-legacy/CHANGELOG.md +++ b/packages/app-legacy/CHANGELOG.md @@ -1,5 +1,169 @@ # example-app-legacy +## 0.2.119-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-org@0.7.0-next.2 + - @backstage/plugin-catalog-graph@0.6.0-next.2 + - @backstage/plugin-mui-to-bui@0.2.5-next.2 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-api-docs@0.13.5-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-catalog-import@0.13.11-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.1 + - @backstage/plugin-devtools@0.1.37-next.2 + - @backstage/plugin-home@0.9.3-next.2 + - @backstage/plugin-home-react@0.1.36-next.1 + - @backstage/plugin-kubernetes@0.12.17-next.2 + - @backstage/plugin-notifications@0.5.15-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.2 + - @backstage/plugin-search@1.6.2-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-signals@0.0.29-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + - @backstage/plugin-user-settings@0.9.1-next.2 + +## 0.2.119-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-mui-to-bui@0.2.5-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## 0.2.119-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.5-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## 0.2.118 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-app-api@0.15.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-api-docs@0.13.4 + - @backstage/plugin-catalog-graph@0.5.7 + - @backstage/plugin-org@0.6.49 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-scaffolder@1.35.3 + - @backstage/plugin-notifications@0.5.14 + - @backstage/plugin-catalog@1.33.0 + - @backstage/plugin-mui-to-bui@0.2.4 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/plugin-kubernetes@0.12.16 + - @backstage/theme@0.7.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.26 + - @backstage/plugin-devtools@0.1.36 + - @backstage/plugin-home@0.9.2 + - @backstage/plugin-search@1.6.0 + - @backstage/plugin-user-settings@0.9.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.33 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-kubernetes-cluster@0.0.34 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-import@0.13.10 + - @backstage/app-defaults@1.7.5 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + - @backstage/plugin-signals@0.0.28 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.2.118-next.2 ### Patch Changes diff --git a/packages/app-legacy/package.json b/packages/app-legacy/package.json index ef8f1a2afb..4dc968be00 100644 --- a/packages/app-legacy/package.json +++ b/packages/app-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-app-legacy", - "version": "0.2.118-next.2", + "version": "0.2.119-next.2", "backstage": { "role": "frontend" }, @@ -92,7 +92,7 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", - "@types/jquery": "^3.3.34", + "@types/jquery": "^4.0.0", "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", diff --git a/packages/app-legacy/src/components/catalog/EntityPage.tsx b/packages/app-legacy/src/components/catalog/EntityPage.tsx index 08d4d921b5..7c42b62542 100644 --- a/packages/app-legacy/src/components/catalog/EntityPage.tsx +++ b/packages/app-legacy/src/components/catalog/EntityPage.tsx @@ -166,11 +166,11 @@ const overviewContent = ( {entityWarningContent} - + - + @@ -299,7 +299,7 @@ const apiPage = ( - + @@ -330,13 +330,10 @@ const userPage = ( {entityWarningContent} - + - + @@ -349,13 +346,10 @@ const groupPage = ( {entityWarningContent} - + - + @@ -374,10 +368,10 @@ const systemPage = ( {entityWarningContent} - + - + @@ -392,7 +386,6 @@ const systemPage = ( {entityWarningContent} - + - + @@ -437,10 +430,10 @@ const resourcePage = ( {entityWarningContent} - + - + diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 01160b1aa6..c11c27519b 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,193 @@ # example-app +## 0.0.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-org@0.7.0-next.2 + - @backstage/plugin-catalog-graph@0.6.0-next.2 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/plugin-app@0.4.1-next.2 + - @backstage/frontend-defaults@0.5.0-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-api-docs@0.13.5-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-app-visualizer@0.2.1-next.2 + - @backstage/plugin-auth@0.1.6-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.1 + - @backstage/plugin-devtools@0.1.37-next.2 + - @backstage/plugin-home@0.9.3-next.2 + - @backstage/plugin-home-react@0.1.36-next.1 + - @backstage/plugin-kubernetes@0.12.17-next.2 + - @backstage/plugin-notifications@0.5.15-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.2 + - @backstage/plugin-search@1.6.2-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-signals@0.0.29-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + - @backstage/plugin-user-settings@0.9.1-next.2 + +## 0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/plugin-app-visualizer@0.2.1-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## 0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/plugin-app-visualizer@0.2.1-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## 0.0.32 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-defaults@0.4.0 + - @backstage/frontend-app-api@0.15.0 + - @backstage/plugin-app-visualizer@0.2.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-api-docs@0.13.4 + - @backstage/plugin-catalog-graph@0.5.7 + - @backstage/plugin-org@0.6.49 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-scaffolder@1.35.3 + - @backstage/plugin-notifications@0.5.14 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/plugin-kubernetes@0.12.16 + - @backstage/core-compat-api@0.5.8 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-app@0.4.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.26 + - @backstage/plugin-devtools@0.1.36 + - @backstage/plugin-home@0.9.2 + - @backstage/plugin-search@1.6.0 + - @backstage/plugin-user-settings@0.9.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.33 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-kubernetes-cluster@0.0.34 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-import@0.13.10 + - @backstage/app-defaults@1.7.5 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + - @backstage/plugin-signals@0.0.28 + - @backstage/plugin-auth@0.1.5 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.0.32-next.2 ### Patch Changes diff --git a/packages/app/app-config.yaml b/packages/app/app-config.yaml deleted file mode 100644 index 2000e785b3..0000000000 --- a/packages/app/app-config.yaml +++ /dev/null @@ -1,267 +0,0 @@ -app: - packages: 'all' # ✨ - - routes: - bindings: - catalog.viewTechDoc: techdocs.docRoot - org.catalogIndex: catalog.catalogIndex - - pluginOverrides: - - match: - pluginId: pages - info: - description: 'This description was overridden in packages/app/app-config.yaml' - - match: - pluginId: /^catalog(-.*)?$/ - info: - ownerEntityRefs: [cubic-belugas] - - match: - packageName: '@backstage/plugin-scaffolder' - info: - ownerEntityRefs: [cubic-belugas] - - extensions: - # set availableLanguages example - - api:app/app-language: - config: - availableLanguages: ['en', 'es', 'fr', 'de', 'ja'] - defaultLanguage: 'en' - - entity-card:org/members-list: - config: - showAggregateMembersToggle: true - initialRelationAggregation: aggregated - - entity-card:org/ownership: - config: - ownedKinds: ['Component', 'API', 'System'] - - # - apis.plugin.graphiql.browse.gitlab: true - # - graphiql-endpoint:graphiql/gitlab: true - - - nav-item:search: false - - nav-item:user-settings: false - - nav-item:catalog - - nav-item:api-docs - - nav-item:scaffolder - - nav-item:app-visualizer - - # Pages - - page:catalog/entity: - config: - showNavItemIcons: true - groups: - # placing a tab at the beginning - - overview: - title: Overview - # example disabling a default group - # - development: false - # example overriding a default group title - - documentation: - title: Docs - icon: docs - - deployment: - title: Deployments - # example adding a new group - - custom: - title: Custom - - # Entity page cards - - entity-card:catalog/about: - config: - type: info - - entity-card:catalog/labels - - entity-card:catalog/links: - config: - filter: - kind: component - metadata.links: - $exists: true - # filter: kind:component has:links - type: info - # - entity-card:linguist/languages - - entity-card:catalog-graph/relations: - config: - height: 300 - - entity-card:api-docs/has-apis - - entity-card:api-docs/consumed-apis - - entity-card:api-docs/provided-apis - - entity-card:api-docs/providing-components - - entity-card:api-docs/consuming-components - # Org Plugin - - entity-card:org/group-profile - - entity-card:org/members-list - - entity-card:org/ownership - - entity-card:org/user-profile: - config: - maxRelations: 5 - hideIcons: true - # - entity-card:azure-devops/readme - - # Entity page contents - - entity-content:catalog/overview: - config: - group: overview - - entity-content:api-docs/definition - - entity-content:api-docs/apis: - config: - # example associating with a default group - group: documentation - icon: kind:api - - entity-content:techdocs: - config: - group: documentation - icon: techdocs - - entity-content:kubernetes/kubernetes: - config: - # example disassociating with a default group - group: false - # - entity-content:azure-devops/pipelines - # - entity-content:azure-devops/pull-requests - # - entity-content:azure-devops/git-tags - - # Enable the catalog-unprocessed-entities tab in devtools - - devtools-content:catalog-unprocessed-entities: true - # Disable the catalog-unprocessed-entities element outside devtools - - page:catalog-unprocessed-entities: false - - # scmAuthExtension: >- - # createScmAuthExtension({ - # id: 'apis.scmAuth.addons.ghe', - # factory({ bind }) { - # bind.scmAuthAddon({ - # baseUrl: 'https://github.spotify.net', - # api: githubAuthApiRef, - # }) - # } - # }) - - # externalRouteRefs: - # bind(catalogPlugin.externalRoutes, { - # createComponent: scaffolderPlugin.routes.root, - # viewTechDoc: techdocsPlugin.routes.docRoot, - # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, - # }); - # bind(apiDocsPlugin.externalRoutes, { - # registerApi: catalogImportPlugin.routes.importPage, - # }); - # bind(scaffolderPlugin.externalRoutes, { - # registerComponent: catalogImportPlugin.routes.importPage, - # viewTechDoc: techdocsPlugin.routes.docRoot, - # }); - # bind(orgPlugin.externalRoutes, { - # catalogIndex: catalogPlugin.routes.catalogIndex, - # }); - - # extensions: - # - plugin.catalog: - # config: - # externalRoutes: - # createComponent: plugin.scaffolder.page - # viewTechDoc: plugin.techdocs.docRootPage - # createFromTemplate: plugin.scaffolder.templatePage - # - graphiql.page: - # config: - # path: / - # - apis.auth.providers.github: - # config: - # provider: ghe - - # - core.signInPage: - # props: - # provider: - # id: google - # title: Google - # message: Sign In using Google - # apiRef: googleAuthApiRef # ??? - - # - core.nav: - # config: - # logo: assets/logo.png - # layout: - # - label: Search - # icon: search - # to: /search - # items: - # - point: search - # - type: divider - # - label: Menu - # icon: menu - # items: - # - label: Home - # icon: home - # to: /catalog - # - label: Create... - # icon: create - # to: /create - # - type: divider - # - type: scroll-wrapper - # items: - # - label: Tech Radar - # icon: map - # to: /tech-radar - # - label: GraphiQL - # icon: graphiql - # to: /graphiql - # - type: divider - # - point: shortcuts - # - type: space - # - type: divider - # - label: Settings - # icon: avatar - # to: /settings - # items: - # - point: settings - # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' - # - core.nav/shortcuts: - # use: '@backstage/plugin-shortcuts#Shortcuts' - # config: - # allowExternalLinks: true - # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' - - # - core.pages.index: - # at: - # point: core.routes - # config: - # path: / - # use: 'react-router-dom#Navigate' - # config: - # to: /catalog - - # - scaffolder.page: - # config: - # groups: - # - title: Recommended - # filter: - # entity.metadata.tags: recommended - # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' - # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' - - # - search.page/content: 'app#customSearchPage' # custom search page from somewhere - - # - user-settings.page.routes.advanced: - # at: - # point: user-settings.page/routes - # config: - # title: Advanced - # path: /advanced - # use: '@backstage/plugin-user-settings#AdvancedSettings' - - # - entity.card.orphanWarning - # - entity.card.processingErrors - # - entity.card.about - # - entity.card.catalogGraph - # - entity.card.pagerDuty - # - entity.card.links - # - entity.card.labels - # - entity.card.githubInsightsLanguages - # - entity.card.githubInsightsReleases - # - entity.card.githubInsightsReadme: - # config: - # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default - # - entity.card.subcomponentsCard - # - entity.card.userProfile - # - entity.card.ownership - # - entity.card.likeDislikeRatings - # - entity.content.dependsOnComponents - # - entity.content.codeInsights - # - entity.content.todo - # - entity.content.feedbackResponse diff --git a/packages/app/package.json b/packages/app/package.json index 586bb17d29..22d7436dc6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.0.32-next.2", + "version": "0.0.33-next.2", "backstage": { "role": "frontend" }, @@ -19,7 +19,7 @@ "build": "backstage-cli package build", "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "start": "backstage-cli package start --config ../../app-config.yaml --config app-config.yaml", + "start": "backstage-cli package start", "test": "backstage-cli package test" }, "browserslist": { @@ -98,7 +98,7 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", - "@types/jquery": "^3.3.34", + "@types/jquery": "^4.0.0", "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", diff --git a/packages/app/src/modules/appModuleNav.tsx b/packages/app/src/modules/appModuleNav.tsx index 9da6358dd3..a507679c73 100644 --- a/packages/app/src/modules/appModuleNav.tsx +++ b/packages/app/src/modules/appModuleNav.tsx @@ -103,34 +103,45 @@ export const appModuleNav = createFrontendModule({ extensions: [ NavContentBlueprint.make({ params: { - component: ({ items }) => ( - - - } to="/search"> - - - - }> - - {items.map((item, index) => ( - - ))} - - - - - - } - to="/settings" - > - - - - - - ), + component: ({ navItems }) => { + const nav = navItems.withComponent(item => ( + item.icon} + to={item.href} + text={item.title} + /> + )); + nav.take('page:home'); // Skip home + return ( + + + } to="/search"> + + + + }> + {nav.take('page:catalog')} + {nav.take('page:scaffolder')} + + + {nav.rest({ sortBy: 'title' })} + + + + + + } + to="/settings" + > + + + + + + ); + }, }, }), ], diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index caa6a2819e..6c392f0d90 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/backend-app-api +## 1.6.0-next.1 + +### Minor Changes + +- 545557a: Registration errors should be forwarded as BackendStartupResult + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.5.0 + +### Minor Changes + +- f1d29b4: Added support for extension point factories, along with the ability to report module startup failures via the extension point factory context. + +### Patch Changes + +- 6bb2f21: Fixed memory leak by properly cleaning up process event listeners on backend shutdown. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 1.5.0-next.1 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 5873c432ff..d65374d9b6 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.5.0-next.1", + "version": "1.6.0-next.1", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 0491faa96f..f782295e71 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -899,7 +899,7 @@ describe('BackendInitializer', () => { }); it('should reject duplicate plugins', async () => { - const init = new BackendInitializer([]); + const init = new BackendInitializer(baseFactories); init.add( createBackendPlugin({ pluginId: 'test', @@ -922,13 +922,24 @@ describe('BackendInitializer', () => { }, }), ); - await expect(init.start()).rejects.toThrow( + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find(p => p.pluginId === 'test'); + expect(plugin?.failure?.error.message).toBe( "Plugin 'test' is already registered", ); + expect(plugin?.failure?.allowed).toBe(false); }); it('should reject duplicate modules', async () => { - const init = new BackendInitializer([]); + const init = new BackendInitializer(baseFactories); init.add(testPlugin); init.add( createBackendModule({ @@ -954,8 +965,202 @@ describe('BackendInitializer', () => { }, }), ); - await expect(init.start()).rejects.toThrow( - "Module 'mod' for plugin 'test' is already registered", + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find(p => p.pluginId === 'test'); + const modResult = plugin?.modules.find( + m => + m.failure?.error.message === + "Module 'mod' for plugin 'test' is already registered", + ); + expect(modResult).toBeDefined(); + expect(modResult?.failure?.allowed).toBe(false); + }); + + it('should allow other plugins to continue when one has a registration error', async () => { + const pluginAInit = jest.fn(async () => {}); + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin-a', + register(reg) { + reg.registerInit({ + deps: {}, + init: pluginAInit, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + // plugin-a should have started successfully + expect(pluginAInit).toHaveBeenCalled(); + const pluginA = err?.result.plugins.find(p => p.pluginId === 'plugin-a'); + expect(pluginA?.failure).toBeUndefined(); + // plugin-b should have a registration failure + const pluginB = err?.result.plugins.find(p => p.pluginId === 'plugin-b'); + expect(pluginB?.failure?.error.message).toBe( + "Plugin 'plugin-b' is already registered", + ); + }); + + it('should permit registration errors for plugins with onPluginBootFailure: continue', async () => { + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { test: { onPluginBootFailure: 'continue' } }, + }, + }, + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + const plugin = result.plugins.find(p => p.pluginId === 'test'); + expect(plugin?.failure?.error.message).toBe( + "Plugin 'test' is already registered", + ); + expect(plugin?.failure?.allowed).toBe(true); + }); + + it('should attribute duplicate extension point errors to the correct plugin', async () => { + const extensionPoint = createExtensionPoint({ id: 'shared-ext' }); + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin-a', + register(reg) { + reg.registerExtensionPoint(extensionPoint, 'a'); + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerExtensionPoint(extensionPoint, 'b'); + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + // plugin-a should succeed (registered first) + const pluginA = err?.result.plugins.find(p => p.pluginId === 'plugin-a'); + expect(pluginA?.failure).toBeUndefined(); + // plugin-b should fail due to duplicate extension point + const pluginB = err?.result.plugins.find(p => p.pluginId === 'plugin-b'); + expect(pluginB?.failure?.error.message).toBe( + "ExtensionPoint with ID 'shared-ext' is already registered", + ); + }); + + it('should attribute invalid registration type errors to plugin when pluginId is available', async () => { + const init = new BackendInitializer(baseFactories); + // Create a fake registration with an invalid type but valid pluginId + const fakeFeature = { + $$type: '@backstage/BackendFeature' as const, + version: 'v1' as const, + featureType: 'registrations' as const, + getRegistrations: () => [ + { + type: 'invalid-type', + pluginId: 'broken-plugin', + init: { deps: {}, func: async () => {} }, + extensionPoints: [], + }, + ], + }; + init.add(fakeFeature as any); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find( + p => p.pluginId === 'broken-plugin', + ); + expect(plugin?.failure?.error.message).toBe( + "Invalid registration type 'invalid-type'", ); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index d51befb644..8ebab723da 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -310,77 +310,6 @@ export class BackendInitializer { // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); - const pluginInits = new Map(); - const moduleInits = new Map>(); - - // Enumerate all registrations - for (const feature of this.#registrations) { - for (const r of feature.getRegistrations()) { - const provides = new Set>(); - - if (r.type === 'plugin' || r.type === 'module') { - // Handle v1 format: Array, unknown]> - for (const [extRef, extImpl] of r.extensionPoints) { - if (this.#extensionPoints.has(extRef.id)) { - throw new Error( - `ExtensionPoint with ID '${extRef.id}' is already registered`, - ); - } - this.#extensionPoints.set(extRef.id, { - pluginId: r.pluginId, - factory: () => extImpl, - }); - provides.add(extRef); - } - } else if (r.type === 'plugin-v1.1' || r.type === 'module-v1.1') { - // Handle v1.1 format: Array - for (const extReg of r.extensionPoints) { - if (this.#extensionPoints.has(extReg.extensionPoint.id)) { - throw new Error( - `ExtensionPoint with ID '${extReg.extensionPoint.id}' is already registered`, - ); - } - this.#extensionPoints.set(extReg.extensionPoint.id, { - pluginId: r.pluginId, - factory: extReg.factory, - }); - provides.add(extReg.extensionPoint); - } - } - - if (r.type === 'plugin' || r.type === 'plugin-v1.1') { - if (pluginInits.has(r.pluginId)) { - throw new Error(`Plugin '${r.pluginId}' is already registered`); - } - pluginInits.set(r.pluginId, { - provides, - consumes: new Set(Object.values(r.init.deps)), - init: r.init, - }); - } else if (r.type === 'module' || r.type === 'module-v1.1') { - let modules = moduleInits.get(r.pluginId); - if (!modules) { - modules = new Map(); - moduleInits.set(r.pluginId, modules); - } - if (modules.has(r.moduleId)) { - throw new Error( - `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`, - ); - } - modules.set(r.moduleId, { - provides, - consumes: new Set(Object.values(r.init.deps)), - init: r.init, - }); - } else { - throw new Error(`Invalid registration type '${(r as any).type}'`); - } - } - } - - const pluginIds = [...pluginInits.keys()]; - const rootConfig = await this.#serviceRegistry.get( coreServices.rootConfig, 'root', @@ -390,15 +319,32 @@ export class BackendInitializer { 'root', ); + const allRegistrations = this.#registrations.flatMap(f => + f.getRegistrations(), + ); + + const allPluginIds = [ + ...new Set( + allRegistrations.flatMap(r => + 'pluginId' in r && typeof r.pluginId === 'string' ? [r.pluginId] : [], + ), + ), + ]; + const resultCollector = createInitializationResultCollector({ - pluginIds, + pluginIds: allPluginIds, logger: rootLogger, allowBootFailurePredicate: createAllowBootFailurePredicate(rootConfig), }); + const { pluginInits, moduleInits } = this.#enumerateRegistrations( + allRegistrations, + resultCollector, + ); + // All plugins are initialized in parallel await Promise.all( - pluginIds.map(async pluginId => { + [...pluginInits.keys()].map(async pluginId => { try { // Initialize all eager services await this.#serviceRegistry.initializeEagerServicesWithScope( @@ -491,6 +437,104 @@ export class BackendInitializer { return { result }; } + #enumerateRegistrations( + allRegistrations: ReturnType< + InternalBackendRegistrations['getRegistrations'] + >, + resultCollector: ReturnType, + ): { + pluginInits: Map; + moduleInits: Map>; + } { + const pluginInits = new Map(); + const moduleInits = new Map>(); + + for (const r of allRegistrations) { + const addedExtensionPointIds: string[] = []; + try { + const provides = new Set>(); + + if (r.type === 'plugin' || r.type === 'module') { + // Handle v1 format: Array, unknown]> + for (const [extRef, extImpl] of r.extensionPoints) { + if (this.#extensionPoints.has(extRef.id)) { + throw new Error( + `ExtensionPoint with ID '${extRef.id}' is already registered`, + ); + } + this.#extensionPoints.set(extRef.id, { + pluginId: r.pluginId, + factory: () => extImpl, + }); + addedExtensionPointIds.push(extRef.id); + provides.add(extRef); + } + } else if (r.type === 'plugin-v1.1' || r.type === 'module-v1.1') { + // Handle v1.1 format: Array + for (const extReg of r.extensionPoints) { + if (this.#extensionPoints.has(extReg.extensionPoint.id)) { + throw new Error( + `ExtensionPoint with ID '${extReg.extensionPoint.id}' is already registered`, + ); + } + this.#extensionPoints.set(extReg.extensionPoint.id, { + pluginId: r.pluginId, + factory: extReg.factory, + }); + addedExtensionPointIds.push(extReg.extensionPoint.id); + provides.add(extReg.extensionPoint); + } + } + + if (r.type === 'plugin' || r.type === 'plugin-v1.1') { + if (pluginInits.has(r.pluginId)) { + throw new Error(`Plugin '${r.pluginId}' is already registered`); + } + pluginInits.set(r.pluginId, { + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); + } else if (r.type === 'module' || r.type === 'module-v1.1') { + let modules = moduleInits.get(r.pluginId); + if (!modules) { + modules = new Map(); + moduleInits.set(r.pluginId, modules); + } + if (modules.has(r.moduleId)) { + throw new Error( + `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`, + ); + } + modules.set(r.moduleId, { + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); + } else { + throw new Error(`Invalid registration type '${(r as any).type}'`); + } + } catch (error: unknown) { + assertError(error); + // Clean up partially registered extension points + for (const id of addedExtensionPointIds) { + this.#extensionPoints.delete(id); + } + if ('pluginId' in r && 'moduleId' in r) { + resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, error); + } else if ('pluginId' in r) { + pluginInits.delete(r.pluginId); + moduleInits.delete(r.pluginId); + resultCollector.onPluginResult(r.pluginId, error); + } else { + throw error; + } + } + } + + return { pluginInits, moduleInits }; + } + // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit async stop(): Promise { instanceRegistry.unregister(this); diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index f11d7c87b6..252c1ea43a 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/backend-defaults +## 0.16.0-next.2 + +### Patch Changes + +- 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available. +- 5fcbef2: Updated dependency `express-rate-limit` to `^8.0.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/backend-app-api@1.6.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 0.16.0-next.1 + +### Minor Changes + +- 0e7d8f9: The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution. +- 527cf88: **BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead. + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.15.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding + an optional `default` type discriminator to PostgreSQL connection configuration, + allowing `config:check` to properly validate `default` connection configurations. +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.15.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 44f5d04: Minor internal restructure of the postgres config loading code +- 4fc7bf0: Bump to tar v7 +- 5dd683f: `createRateLimitMiddleware` is now exported from `@backstage/backend-defaults/httpRouter` +- 8dd518a: Support `connection.type: azure` in database client to use Microsoft Entra authentication with Azure database for PostgreSQL +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-app-api@1.5.0 + - @backstage/integration@1.20.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config-loader@1.10.8 + - @backstage/cli-node@0.2.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + ## 0.15.2-next.1 ### Patch Changes diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index f978fa5924..3d09146b0d 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -633,6 +633,10 @@ export interface Config { ipAddressType?: 'PUBLIC' | 'PRIVATE' | 'PSC'; } | { + /** + * The rest config for default, regular connections + */ + type?: 'default'; /** * Password that belongs to the client User * @visibility secret @@ -1123,6 +1127,36 @@ export interface Config { headers?: { [name: string]: string }; }; + /** + * Options for the metrics service. + */ + metrics?: { + /** + * Plugin-specific metrics configuration. Each plugin can override meter metadata. + */ + plugin?: { + [pluginId: string]: { + /** + * Meter configuration for this plugin. + */ + meter?: { + /** + * Custom meter name. If not set, defaults to backstage-plugin-{pluginId}. + */ + name?: string; + /** + * Version for the meter. + */ + version?: string; + /** + * Schema URL for the meter. + */ + schemaUrl?: string; + }; + }; + }; + }; + /** * Options to configure the default RootLoggerService. */ diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 9ecf7daf6a..fb4e29a3ce 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.15.2-next.1", + "version": "0.16.0-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" @@ -144,6 +144,7 @@ "@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/storage": "^7.0.0", @@ -164,7 +165,7 @@ "cron": "^3.0.0", "express": "^4.22.0", "express-promise-router": "^4.1.0", - "express-rate-limit": "^7.5.0", + "express-rate-limit": "^8.2.2", "fs-extra": "^11.2.0", "git-url-parse": "^15.0.0", "helmet": "^6.0.0", @@ -176,7 +177,7 @@ "lodash": "^4.17.21", "logform": "^2.3.2", "luxon": "^3.0.0", - "minimatch": "^9.0.0", + "minimatch": "^10.2.1", "mysql2": "^3.0.0", "node-fetch": "^2.7.0", "node-forge": "^1.3.2", diff --git a/packages/backend-defaults/report-alpha.api.md b/packages/backend-defaults/report-alpha.api.md index c40517863a..352ec1d688 100644 --- a/packages/backend-defaults/report-alpha.api.md +++ b/packages/backend-defaults/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { RootSystemMetadataService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -22,6 +23,13 @@ export const actionsServiceFactory: ServiceFactory< 'singleton' >; +// @alpha +export const metricsServiceFactory: ServiceFactory< + MetricsService, + 'plugin', + 'singleton' +>; + // @alpha export const rootSystemMetadataServiceFactory: ServiceFactory< RootSystemMetadataService, diff --git a/packages/backend-defaults/report-scheduler.api.md b/packages/backend-defaults/report-scheduler.api.md index e0a85710a5..44a503d18c 100644 --- a/packages/backend-defaults/report-scheduler.api.md +++ b/packages/backend-defaults/report-scheduler.api.md @@ -6,6 +6,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -17,6 +18,7 @@ export class DefaultSchedulerService { static create(options: { database: DatabaseService; logger: LoggerService; + metrics: MetricsService; rootLifecycle: RootLifecycleService; httpRouter: HttpRouterService; pluginMetadata: PluginMetadataService; diff --git a/packages/backend-defaults/report-urlReader.api.md b/packages/backend-defaults/report-urlReader.api.md index 78a35911b8..c32a63231b 100644 --- a/packages/backend-defaults/report-urlReader.api.md +++ b/packages/backend-defaults/report-urlReader.api.md @@ -10,7 +10,6 @@ import { AzureCredentialsManager } from '@backstage/integration'; import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; import { BitbucketCloudIntegration } from '@backstage/integration'; -import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import { GerritIntegration } from '@backstage/integration'; @@ -190,38 +189,6 @@ export class BitbucketServerUrlReader implements UrlReaderService { toString(): string; } -// @public @deprecated -export class BitbucketUrlReader implements UrlReaderService { - constructor( - integration: BitbucketIntegration, - logger: LoggerService, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree( - url: string, - options?: UrlReaderServiceReadTreeOptions, - ): Promise; - // (undocumented) - readUrl( - url: string, - options?: UrlReaderServiceReadUrlOptions, - ): Promise; - // (undocumented) - search( - url: string, - options?: UrlReaderServiceSearchOptions, - ): Promise; - // (undocumented) - toString(): string; -} - // @public export class FetchUrlReader implements UrlReaderService { static factory: ReaderFactory; diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 9f8116a996..2b4ee5befa 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -38,6 +38,7 @@ import { eventsServiceFactory } from '@backstage/plugin-events-node'; import { actionsRegistryServiceFactory, actionsServiceFactory, + metricsServiceFactory, } from '@backstage/backend-defaults/alpha'; import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory'; @@ -66,6 +67,7 @@ export const defaultServiceFactories = [ // alpha services actionsRegistryServiceFactory, actionsServiceFactory, + metricsServiceFactory, // Unexported alpha services kept around for compatibility reasons instanceMetadataServiceFactory, diff --git a/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts index dbce47bea4..3f600dcbbc 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actions/actionsServiceFactory.test.ts @@ -66,6 +66,7 @@ describe('actionsServiceFactory', () => { const mockActionsDefinition: ActionsServiceAction = { description: 'my mock description', id: 'my-plugin:test', + pluginId: 'my-plugin', name: 'testy', title: 'Test', schema: { @@ -755,6 +756,7 @@ describe('actionsServiceFactory', () => { { description: 'Test', id: 'plugin-with-action:with-validation', + pluginId: 'plugin-with-action', name: 'with-validation', schema: { input: { diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index 7348e0a185..a2109ec1bf 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -16,8 +16,11 @@ import { AuthService, + BackstageCredentials, HttpAuthService, LoggerService, + PermissionsRegistryService, + PermissionsService, PluginMetadataService, } from '@backstage/backend-plugin-api'; import PromiseRouter from 'express-promise-router'; @@ -28,12 +31,10 @@ import { ActionsRegistryActionOptions, ActionsRegistryService, } from '@backstage/backend-plugin-api/alpha'; -import { - ForwardedError, - InputError, - NotAllowedError, - NotFoundError, -} from '@backstage/errors'; +import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +type ActionEntry = [string, ActionsRegistryActionOptions]; export class DefaultActionsRegistryService implements ActionsRegistryService { private actions: Map> = @@ -43,17 +44,23 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { private readonly httpAuth: HttpAuthService; private readonly auth: AuthService; private readonly metadata: PluginMetadataService; + private readonly permissions: PermissionsService; + private readonly permissionsRegistry: PermissionsRegistryService; private constructor( logger: LoggerService, httpAuth: HttpAuthService, auth: AuthService, metadata: PluginMetadataService, + permissions: PermissionsService, + permissionsRegistry: PermissionsRegistryService, ) { this.logger = logger; this.httpAuth = httpAuth; this.auth = auth; this.metadata = metadata; + this.permissions = permissions; + this.permissionsRegistry = permissionsRegistry; } static create({ @@ -61,24 +68,46 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { logger, auth, metadata, + permissions, + permissionsRegistry, }: { httpAuth: HttpAuthService; logger: LoggerService; auth: AuthService; metadata: PluginMetadataService; + permissions: PermissionsService; + permissionsRegistry: PermissionsRegistryService; }): DefaultActionsRegistryService { - return new DefaultActionsRegistryService(logger, httpAuth, auth, metadata); + return new DefaultActionsRegistryService( + logger, + httpAuth, + auth, + metadata, + permissions, + permissionsRegistry, + ); } createRouter(): Router { const router = PromiseRouter(); router.use(json()); - router.get('/.backstage/actions/v1/actions', (_, res) => { + router.get('/.backstage/actions/v1/actions', async (req, res) => { + const credentials = await this.httpAuth.credentials(req); + const entries = Array.from(this.actions.entries()); + + const allowedActions = await this.filterByPermissions( + entries, + credentials, + ); + return res.json({ - actions: Array.from(this.actions.entries()).map(([id, action]) => ({ + actions: allowedActions.map(([id, action]) => ({ id, - ...action, + name: action.name, + title: action.title, + description: action.description, + pluginId: this.metadata.getId(), attributes: { // Inspired by the @modelcontextprotocol/sdk defaults for the hints. // https://github.com/modelcontextprotocol/typescript-sdk/blob/dd69efa1de8646bb6b195ff8d5f52e13739f4550/src/types.ts#L777-L812 @@ -120,6 +149,18 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { throw new NotFoundError(`Action "${req.params.actionId}" not found`); } + if (action.visibilityPermission) { + const [decision] = await this.permissions.authorize( + [{ permission: action.visibilityPermission }], + { credentials }, + ); + if (decision.result !== AuthorizeResult.ALLOW) { + throw new NotFoundError( + `Action "${req.params.actionId}" not found`, + ); + } + } + const input = action.schema?.input ? action.schema.input(z).safeParse(req.body) : ({ success: true, data: undefined } as const); @@ -131,31 +172,24 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { ); } - try { - const result = await action.action({ - input: input.data, - credentials, - logger: this.logger, - }); + const result = await action.action({ + input: input.data, + credentials, + logger: this.logger, + }); - const output = action.schema?.output - ? action.schema.output(z).safeParse(result?.output) - : ({ success: true, data: result?.output } as const); + const output = action.schema?.output + ? action.schema.output(z).safeParse(result?.output) + : ({ success: true, data: result?.output } as const); - if (!output.success) { - throw new InputError( - `Invalid output from action "${req.params.actionId}"`, - output.error, - ); - } - - res.json({ output: output.data }); - } catch (error) { - throw new ForwardedError( - `Failed execution of action "${req.params.actionId}"`, - error, + if (!output.success) { + throw new InputError( + `Invalid output from action "${req.params.actionId}"`, + output.error, ); } + + res.json({ output: output.data }); }, ); return router; @@ -171,6 +205,38 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { throw new Error(`Action with id "${id}" is already registered`); } + if (options.visibilityPermission) { + this.permissionsRegistry.addPermissions([options.visibilityPermission]); + } + this.actions.set(id, options); } + + private async filterByPermissions( + entries: ActionEntry[], + credentials: BackstageCredentials, + ): Promise { + const permissionedEntries = entries.filter( + ([_, action]) => action.visibilityPermission, + ); + + if (permissionedEntries.length === 0) { + return entries; + } + + const decisions = await this.permissions.authorize( + permissionedEntries.map(([_, action]) => ({ + permission: action.visibilityPermission!, + })), + { credentials }, + ); + + const deniedIds = new Set( + permissionedEntries + .filter((_, index) => decisions[index].result !== AuthorizeResult.ALLOW) + .map(([id]) => id), + ); + + return entries.filter(([id]) => !deniedIds.has(id)); + } } diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts index 79c103c284..56327031ec 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts @@ -22,8 +22,12 @@ import { import { httpRouterServiceFactory } from '../../../entrypoints/httpRouter'; import request from 'supertest'; import { actionsRegistryServiceFactory } from './actionsRegistryServiceFactory'; -import { InputError } from '@backstage/errors'; +import { InputError, NotFoundError } from '@backstage/errors'; import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + AuthorizeResult, + createPermission, +} from '@backstage/plugin-permission-common'; describe('actionsRegistryServiceFactory', () => { const defaultServices = [ @@ -510,7 +514,7 @@ describe('actionsRegistryServiceFactory', () => { expect(body).toMatchObject({ output: { ok: true } }); }); - it('should return the error from the action if it throws', async () => { + it('should forward the original error when the action throws a known error', async () => { const { server } = await startTestBackend({ features: [pluginSubject, ...defaultServices], }); @@ -528,11 +532,341 @@ describe('actionsRegistryServiceFactory', () => { expect(status).toBe(400); expect(body).toMatchObject({ error: { - message: expect.stringContaining( - 'Failed execution of action "my-plugin:test"', - ), + name: 'InputError', + message: 'test', + }, + }); + }); + + it('should forward a NotFoundError from the action with 404 status', async () => { + const { server } = await startTestBackend({ + features: [pluginSubject, ...defaultServices], + }); + + mockAction.mockRejectedValue(new NotFoundError('entity not found')); + + const { body, status } = await request(server) + .post( + '/api/my-plugin/.backstage/actions/v1/actions/my-plugin:test/invoke', + ) + .send({ + name: 'test', + }); + + expect(status).toBe(404); + expect(body).toMatchObject({ + error: { + name: 'NotFoundError', + message: 'entity not found', }, }); }); }); + + describe('permissions', () => { + const testPermission = createPermission({ + name: 'test.action.use', + attributes: {}, + }); + + it('should filter out actions with denied permissions when listing', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'public-action', + title: 'Public Action', + description: 'No permission required', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.permissions.factory({ + result: AuthorizeResult.DENY, + }), + ], + }); + + const { body, status } = await request(server).get( + '/api/my-plugin/.backstage/actions/v1/actions', + ); + + expect(status).toBe(200); + expect(body.actions).toHaveLength(1); + expect(body.actions[0].name).toBe('public-action'); + }); + + it('should include actions with allowed permissions when listing', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.permissions.factory({ + result: AuthorizeResult.ALLOW, + }), + ], + }); + + const { body, status } = await request(server).get( + '/api/my-plugin/.backstage/actions/v1/actions', + ); + + expect(status).toBe(200); + expect(body.actions).toHaveLength(1); + expect(body.actions[0].name).toBe('protected-action'); + }); + + it('should return 404 when invoking an action with denied permission', async () => { + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.permissions.factory({ + result: AuthorizeResult.DENY, + }), + ], + }); + + const { body, status } = await request(server).post( + '/api/my-plugin/.backstage/actions/v1/actions/my-plugin:protected-action/invoke', + ); + + expect(status).toBe(404); + expect(body).toMatchObject({ + error: { + message: 'Action "my-plugin:protected-action" not found', + }, + }); + }); + + it('should allow invoking an action when permission is granted', async () => { + const mockAction = jest.fn().mockResolvedValue({ output: { ok: true } }); + + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({ ok: z.boolean() }), + }, + action: mockAction, + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + mockServices.permissions.factory({ + result: AuthorizeResult.ALLOW, + }), + ], + }); + + const { body, status } = await request(server).post( + '/api/my-plugin/.backstage/actions/v1/actions/my-plugin:protected-action/invoke', + ); + + expect(status).toBe(200); + expect(body).toMatchObject({ output: { ok: true } }); + expect(mockAction).toHaveBeenCalled(); + }); + + it('should pass the correct permission to the authorize call', async () => { + const permissionsMock = mockServices.permissions.mock({ + authorize: async () => [{ result: AuthorizeResult.ALLOW }], + }); + + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + }, + }); + }, + }); + + const { server } = await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + permissionsMock.factory, + ], + }); + + await request(server).get('/api/my-plugin/.backstage/actions/v1/actions'); + + expect(permissionsMock.authorize).toHaveBeenCalledWith( + [{ permission: testPermission }], + expect.objectContaining({ credentials: expect.anything() }), + ); + }); + + it('should register the permission with the permissions registry', async () => { + const permissionsRegistryMock = mockServices.permissionsRegistry.mock(); + + const pluginSubject = createBackendPlugin({ + pluginId: 'my-plugin', + register(reg) { + reg.registerInit({ + deps: { + actionsRegistry: actionsRegistryServiceRef, + }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'protected-action', + title: 'Protected Action', + description: 'Permission required', + visibilityPermission: testPermission, + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + }, + }); + }, + }); + + await startTestBackend({ + features: [ + pluginSubject, + actionsRegistryServiceFactory, + httpRouterServiceFactory, + mockServices.httpAuth.factory({ + defaultCredentials: mockCredentials.service('user:default/mock'), + }), + permissionsRegistryMock.factory, + ], + }); + + expect(permissionsRegistryMock.addPermissions).toHaveBeenCalledWith([ + testPermission, + ]); + }); + }); }); diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts index 8c19b8148c..4ac6b2c782 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.ts @@ -32,13 +32,25 @@ export const actionsRegistryServiceFactory = createServiceFactory({ httpAuth: coreServices.httpAuth, logger: coreServices.logger, auth: coreServices.auth, + permissions: coreServices.permissions, + permissionsRegistry: coreServices.permissionsRegistry, }, - factory: ({ metadata, httpRouter, httpAuth, logger, auth }) => { + factory: ({ + metadata, + httpRouter, + httpAuth, + logger, + auth, + permissions, + permissionsRegistry, + }) => { const actionsRegistryService = DefaultActionsRegistryService.create({ httpAuth, logger, auth, metadata, + permissions, + permissionsRegistry, }); httpRouter.use(actionsRegistryService.createRouter()); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts new file mode 100644 index 0000000000..75e5104754 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2026 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 { metrics } from '@opentelemetry/api'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +const mockGetMeter = jest.spyOn(metrics, 'getMeter'); + +describe('DefaultMetricsService', () => { + beforeEach(() => { + mockGetMeter.mockClear(); + }); + + describe('create', () => { + it('should create a MetricsService with name only', () => { + const service = DefaultMetricsService.create({ name: 'test-meter' }); + + expect(mockGetMeter).toHaveBeenCalledTimes(1); + expect(mockGetMeter).toHaveBeenCalledWith('test-meter', undefined, { + schemaUrl: undefined, + }); + + expect(service).toBeDefined(); + }); + + it('should create a MetricsService with name, version, and schemaUrl', () => { + const service = DefaultMetricsService.create({ + name: 'test-meter', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }); + + expect(mockGetMeter).toHaveBeenCalledTimes(1); + expect(mockGetMeter).toHaveBeenCalledWith('test-meter', '1.2.3', { + schemaUrl: 'https://example.com/schema', + }); + + expect(service).toBeDefined(); + }); + }); + + describe('metric instruments', () => { + it('should create a counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createCounter('my_counter', { + description: 'A test counter', + unit: 'bytes', + }); + + expect(counter).toBeDefined(); + expect(counter.add).toBeDefined(); + }); + + it('should create an up-down counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const upDownCounter = service.createUpDownCounter('my_updown'); + + expect(upDownCounter).toBeDefined(); + expect(upDownCounter.add).toBeDefined(); + }); + + it('should create a histogram', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const histogram = service.createHistogram('my_histogram'); + + expect(histogram).toBeDefined(); + expect(histogram.record).toBeDefined(); + }); + + it('should create a gauge', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const gauge = service.createGauge('my_gauge'); + + expect(gauge).toBeDefined(); + expect(gauge.record).toBeDefined(); + }); + + it('should create an observable counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createObservableCounter('my_observable_counter'); + + expect(counter).toBeDefined(); + expect(counter.addCallback).toBeDefined(); + expect(counter.removeCallback).toBeDefined(); + }); + + it('should create an observable up-down counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createObservableUpDownCounter( + 'my_observable_updown', + ); + + expect(counter).toBeDefined(); + expect(counter.addCallback).toBeDefined(); + }); + + it('should create an observable gauge', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const gauge = service.createObservableGauge('my_observable_gauge'); + + expect(gauge).toBeDefined(); + expect(gauge.addCallback).toBeDefined(); + }); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts new file mode 100644 index 0000000000..764c6d6d1c --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2026 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 { Meter, metrics } from '@opentelemetry/api'; +import { + MetricsService, + MetricAttributes, + MetricOptions, + MetricsServiceCounter, + MetricsServiceUpDownCounter, + MetricsServiceHistogram, + MetricsServiceGauge, + MetricsServiceObservableCounter, + MetricsServiceObservableGauge, + MetricsServiceObservableUpDownCounter, +} from '@backstage/backend-plugin-api/alpha'; + +/** + * Options for creating a {@link DefaultMetricsService}. + * + * @alpha + */ +export interface DefaultMetricsServiceOptions { + name: string; + version?: string; + schemaUrl?: string; +} + +/** + * Default implementation of the {@link MetricsService} interface. + * + * This implementation provides a thin wrapper around the OpenTelemetry Meter API. + * + * @alpha + */ +export class DefaultMetricsService implements MetricsService { + private readonly meter: Meter; + + private constructor(opts: DefaultMetricsServiceOptions) { + // The meter name sets the OpenTelemetry Instrumentation Scope which identifies the source of metrics in telemetry backends. + this.meter = metrics.getMeter(opts.name, opts.version, { + schemaUrl: opts.schemaUrl, + }); + } + + /** + * Creates a new {@link MetricsService} instance. + * + * @param opts - Options for configuring the meter scope + * @returns A new MetricsService instance + */ + static create(opts: DefaultMetricsServiceOptions): MetricsService { + return new DefaultMetricsService(opts); + } + + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter { + return this.meter.createCounter(name, opts); + } + + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter { + return this.meter.createUpDownCounter(name, opts); + } + + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram { + return this.meter.createHistogram(name, opts); + } + + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge { + return this.meter.createGauge(name, opts); + } + + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter { + return this.meter.createObservableCounter(name, opts); + } + + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter { + return this.meter.createObservableUpDownCounter(name, opts); + } + + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge { + return this.meter.createObservableGauge(name, opts); + } +} diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts new file mode 100644 index 0000000000..fc3e04a855 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2026 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 { metricsServiceFactory } from './metricsServiceFactory'; diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts new file mode 100644 index 0000000000..854fcfd389 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2026 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 { + mockServices, + ServiceFactoryTester, +} from '@backstage/backend-test-utils'; +import { metricsServiceFactory } from './metricsServiceFactory'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +describe('metricsServiceFactory', () => { + let createSpy: jest.SpyInstance; + + beforeEach(() => { + createSpy = jest.spyOn(DefaultMetricsService, 'create'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const defaultServices = [ + mockServices.rootConfig.factory(), + metricsServiceFactory, + ]; + + it('should use backstage-plugin-{pluginId} as meter name when no config is set', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: defaultServices, + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'backstage-plugin-my-plugin', + version: undefined, + schemaUrl: undefined, + }); + }); + + it('should use custom name from config', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + metrics: { + plugin: { + 'my-plugin': { + meter: { + name: 'custom-metrics-name', + }, + }, + }, + }, + }, + }, + }), + metricsServiceFactory, + ], + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'custom-metrics-name', + version: undefined, + schemaUrl: undefined, + }); + }); + + it('should accept version and schemaUrl from config', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + metrics: { + plugin: { + 'my-plugin': { + meter: { + name: 'my-plugin-metrics', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }, + }, + }, + }, + }, + }, + }), + metricsServiceFactory, + ], + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'my-plugin-metrics', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }); + }); + + it('should implement the full MetricsService interface', async () => { + const subject = await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: defaultServices, + }).getSubject('test-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'backstage-plugin-test-plugin', + version: undefined, + schemaUrl: undefined, + }); + + expect(subject.createCounter).toBeDefined(); + expect(subject.createUpDownCounter).toBeDefined(); + expect(subject.createHistogram).toBeDefined(); + expect(subject.createGauge).toBeDefined(); + expect(subject.createObservableCounter).toBeDefined(); + expect(subject.createObservableUpDownCounter).toBeDefined(); + expect(subject.createObservableGauge).toBeDefined(); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts new file mode 100644 index 0000000000..a62ad0411a --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +/** + * Service factory for collecting plugin-scoped metrics. + * + * @alpha + */ +export const metricsServiceFactory = createServiceFactory({ + service: metricsServiceRef, + deps: { + config: coreServices.rootConfig, + pluginMetadata: coreServices.pluginMetadata, + }, + factory: ({ config, pluginMetadata }) => { + const pluginId = pluginMetadata.getId(); + + const meterConfig = config.getOptionalConfig( + `backend.metrics.plugin.${pluginId}.meter`, + ); + const scopeName = `backstage-plugin-${pluginId}`; + const name = meterConfig?.getOptionalString('name') ?? scopeName; + const version = meterConfig?.getOptionalString('version'); + const schemaUrl = meterConfig?.getOptionalString('schemaUrl'); + + return DefaultMetricsService.create({ name, version, schemaUrl }); + }, +}); diff --git a/packages/backend-defaults/src/alpha/index.ts b/packages/backend-defaults/src/alpha/index.ts index c17e0e71cb..1d0ec158af 100644 --- a/packages/backend-defaults/src/alpha/index.ts +++ b/packages/backend-defaults/src/alpha/index.ts @@ -16,4 +16,5 @@ export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry'; export { actionsServiceFactory } from './entrypoints/actions'; +export { metricsServiceFactory } from './entrypoints/metrics'; export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata'; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts index 61f9bd0639..3de69ba566 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts @@ -20,6 +20,7 @@ import waitForExpect from 'wait-for-expect'; import { DefaultSchedulerService } from './DefaultSchedulerService'; import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -32,6 +33,7 @@ describe('TaskScheduler', () => { getId: () => 'test', } satisfies PluginMetadataService; const testScopedSignal = createTestScopedSignal(); + const metrics = metricsServiceMock.mock(); it.each(databases.eachSupportedId())( 'can return a working v1 plugin impl, %p', @@ -42,6 +44,7 @@ describe('TaskScheduler', () => { const manager = DefaultSchedulerService.create({ database, logger, + metrics, rootLifecycle, httpRouter, pluginMetadata, @@ -71,6 +74,7 @@ describe('TaskScheduler', () => { const manager = DefaultSchedulerService.create({ database, logger, + metrics, rootLifecycle, httpRouter, pluginMetadata, diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts index 0a7b9fbb87..07531cffc8 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts @@ -27,6 +27,7 @@ import { Duration } from 'luxon'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; /** * Default implementation of the task scheduler service. @@ -37,6 +38,7 @@ export class DefaultSchedulerService { static create(options: { database: DatabaseService; logger: LoggerService; + metrics: MetricsService; rootLifecycle: RootLifecycleService; httpRouter: HttpRouterService; pluginMetadata: PluginMetadataService; @@ -67,6 +69,7 @@ export class DefaultSchedulerService { options.pluginMetadata.getId(), databaseFactory, options.logger, + options.metrics, options.rootLifecycle, ); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts index 14fedfb3c0..04c2b17ad3 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts @@ -16,6 +16,7 @@ import { LocalTaskWorker } from './LocalTaskWorker'; import { mockServices } from '@backstage/backend-test-utils'; +import { ConflictError } from '@backstage/errors'; import waitFor from 'wait-for-expect'; jest.setTimeout(10_000); @@ -110,6 +111,54 @@ describe('LocalTaskWorker', () => { controller.abort(); }); + it('can cancel a running task', async () => { + let receivedSignal: AbortSignal | undefined; + const fn = jest.fn(async (signal: AbortSignal) => { + receivedSignal = signal; + await new Promise(r => setTimeout(r, 5000)); + }); + const controller = new AbortController(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + cadence: 'PT10S', + timeoutAfterDuration: 'PT10S', + }, + { signal: controller.signal }, + ); + + await waitFor(() => { + expect(fn).toHaveBeenCalledTimes(1); + }); + + expect(receivedSignal?.aborted).toBe(false); + worker.cancel(); + expect(receivedSignal?.aborted).toBe(true); + + controller.abort(); + }); + + it('cannot cancel a task that is not running', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT1000S', + cadence: 'PT10S', + timeoutAfterDuration: 'PT10S', + }, + { signal: controller.signal }, + ); + + expect(() => worker.cancel()).toThrow(ConflictError); + controller.abort(); + }); + it('goes through the expected states', async () => { const fn = jest .fn() diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts index efecb1c0ed..168649436c 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts @@ -29,6 +29,7 @@ import { delegateAbortController, serializeError, sleep } from './util'; */ export class LocalTaskWorker { private abortWait: AbortController | undefined; + private taskAbortController: AbortController | undefined; #taskState: Exclude = { status: 'idle', }; @@ -93,6 +94,13 @@ export class LocalTaskWorker { this.abortWait.abort(); } + cancel(): void { + if (!this.taskAbortController) { + throw new ConflictError(`Task ${this.taskId} is not running`); + } + this.taskAbortController.abort(); + } + taskState(): TaskApiTasksResponse['taskState'] { return this.#taskState; } @@ -134,10 +142,10 @@ export class LocalTaskWorker { ): Promise { // Abort the task execution either if the worker is stopped, or if the // task timeout is hit - const taskAbortController = delegateAbortController(signal); + this.taskAbortController = delegateAbortController(signal); const timeoutDuration = Duration.fromISO(settings.timeoutAfterDuration); const timeoutHandle = setTimeout(() => { - taskAbortController.abort(); + this.taskAbortController?.abort(); }, timeoutDuration.as('milliseconds')); this.#taskState = { @@ -152,7 +160,7 @@ export class LocalTaskWorker { }; try { - await this.fn(taskAbortController.signal); + await this.fn(this.taskAbortController.signal); this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!; this.#taskState.lastRunError = undefined; } catch (e) { @@ -162,7 +170,8 @@ export class LocalTaskWorker { // release resources clearTimeout(timeoutHandle); - taskAbortController.abort(); + this.taskAbortController.abort(); + this.taskAbortController = undefined; } /** diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index 5872ae793e..cc8aac5f51 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -27,6 +27,7 @@ import { parseDuration, } from './PluginTaskSchedulerImpl'; import { createDeferred } from '@backstage/types'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -56,6 +57,7 @@ describe('PluginTaskManagerImpl', () => { 'myplugin', async () => knex, mockServices.logger.mock(), + metricsServiceMock.mock(), { addShutdownHook, addBeforeShutdownHook: jest.fn(), @@ -397,6 +399,100 @@ describe('PluginTaskManagerImpl', () => { ); }); + describe('cancelTask with local scope', () => { + it('can cancel a running task', async () => { + const { manager } = await init('SQLITE_3'); + + const promise = createDeferred(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn: async () => { + promise.resolve(); + await new Promise(r => setTimeout(r, 20000)); + }, + scope: 'local', + }); + + await promise; + await expect(manager.cancelTask('task1')).resolves.toBeUndefined(); + }, 60_000); + + it('cannot cancel a task that is not running', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn, + scope: 'local', + }); + + await expect(manager.cancelTask('task1')).rejects.toThrow(ConflictError); + }, 60_000); + }); + + describe('cancelTask with global scope', () => { + it.each(databases.eachSupportedId())( + 'can cancel a running task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const promise = createDeferred(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn: async () => { + promise.resolve(); + await new Promise(r => setTimeout(r, 20000)); + }, + scope: 'global', + }); + + await promise; + await expect(manager.cancelTask('task1')).resolves.toBeUndefined(); + }, + ); + + it.each(databases.eachSupportedId())( + 'cannot cancel a non-existent task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + await expect(manager.cancelTask('nonexistent')).rejects.toThrow( + NotFoundError, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'cannot cancel a task that is not running, %p', + async databaseId => { + const { manager } = await init(databaseId); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn: jest.fn(), + scope: 'global', + }); + + await expect(manager.cancelTask('task1')).rejects.toThrow( + ConflictError, + ); + }, + ); + }); + describe('parseDuration', () => { it('should parse durations', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 90f5d08e4a..2aad8647fe 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -24,7 +24,13 @@ import { SchedulerServiceTaskRunner, SchedulerServiceTaskScheduleDefinition, } from '@backstage/backend-plugin-api'; -import { Counter, Histogram, Gauge, metrics, trace } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import { + MetricsService, + MetricsServiceCounter, + MetricsServiceGauge, + MetricsServiceHistogram, +} from '@backstage/backend-plugin-api/alpha'; import { Knex } from 'knex'; import { Duration } from 'luxon'; import express from 'express'; @@ -45,10 +51,10 @@ export class PluginTaskSchedulerImpl implements SchedulerService { private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = []; private readonly shutdownInitiated: Promise; - private readonly counter: Counter; - private readonly duration: Histogram; - private readonly lastStarted: Gauge; - private readonly lastCompleted: Gauge; + private readonly counter: MetricsServiceCounter; + private readonly duration: MetricsServiceHistogram; + private readonly lastStarted: MetricsServiceGauge; + private readonly lastCompleted: MetricsServiceGauge; private readonly pluginId: string; private readonly databaseFactory: () => Promise; @@ -58,24 +64,27 @@ export class PluginTaskSchedulerImpl implements SchedulerService { pluginId: string, databaseFactory: () => Promise, logger: LoggerService, + metrics: MetricsService, rootLifecycle: RootLifecycleService, ) { this.pluginId = pluginId; this.databaseFactory = databaseFactory; this.logger = logger; - const meter = metrics.getMeter('default'); - this.counter = meter.createCounter('backend_tasks.task.runs.count', { + this.counter = metrics.createCounter('backend_tasks.task.runs.count', { description: 'Total number of times a task has been run', }); - this.duration = meter.createHistogram('backend_tasks.task.runs.duration', { - description: 'Histogram of task run durations', - unit: 'seconds', - }); - this.lastStarted = meter.createGauge('backend_tasks.task.runs.started', { + this.duration = metrics.createHistogram( + 'backend_tasks.task.runs.duration', + { + description: 'Histogram of task run durations', + unit: 'seconds', + }, + ); + this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', { description: 'Epoch timestamp seconds when the task was last started', unit: 'seconds', }); - this.lastCompleted = meter.createGauge( + this.lastCompleted = metrics.createGauge( 'backend_tasks.task.runs.completed', { description: 'Epoch timestamp seconds when the task was last completed', @@ -98,6 +107,17 @@ export class PluginTaskSchedulerImpl implements SchedulerService { await TaskWorker.trigger(knex, id); } + async cancelTask(id: string): Promise { + const localTask = this.localWorkersById.get(id); + if (localTask) { + localTask.cancel(); + return; + } + + const knex = await this.databaseFactory(); + await TaskWorker.cancel(knex, id); + } + async scheduleTask( task: SchedulerServiceTaskScheduleDefinition & SchedulerServiceTaskInvocationDefinition, @@ -197,6 +217,15 @@ export class PluginTaskSchedulerImpl implements SchedulerService { }, ); + router.post( + '/.backstage/scheduler/v1/tasks/:id/cancel', + async (req, res) => { + const { id } = req.params; + await this.cancelTask(id); + res.status(200).end(); + }, + ); + return router; } diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts index 5bd3fb38cb..3446d635d1 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -15,6 +15,7 @@ */ import { TestDatabases, mockServices } from '@backstage/backend-test-utils'; +import { ConflictError, NotFoundError } from '@backstage/errors'; import { DateTime, Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; @@ -584,4 +585,79 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'can cancel a running task, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn(async () => {}); + const settings: TaskSettingsV2 = { + version: 2, + cadence: '* * * * * *', + initialDelayDuration: undefined, + timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO()!, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + await worker.tryClaimTask('ticket', settings); + + // Verify the task is running + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.current_run_ticket).toBe('ticket'); + + await TaskWorker.cancel(knex, 'task1'); + + // Verify the task is now idle with a cancellation error recorded + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.current_run_ticket).toBeNull(); + expect(row.current_run_started_at).toBeNull(); + expect(row.current_run_expires_at).toBeNull(); + expect(row.last_run_ended_at).not.toBeNull(); + expect(row.last_run_error_json).toContain('Task was cancelled'); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + 'cannot cancel a non-existent task, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + await expect(TaskWorker.cancel(knex, 'nonexistent')).rejects.toThrow( + NotFoundError, + ); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + 'cannot cancel a task that is not running, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn(async () => {}); + const settings: TaskSettingsV2 = { + version: 2, + cadence: '* * * * * *', + initialDelayDuration: undefined, + timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO()!, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + + await expect(TaskWorker.cancel(knex, 'task1')).rejects.toThrow( + ConflictError, + ); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts index e47967de90..4f32dc6863 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -152,6 +152,36 @@ export class TaskWorker { } } + static async cancel(knex: Knex, taskId: string): Promise { + const [row] = await knex(DB_TASKS_TABLE) + .where('id', '=', taskId) + .select('settings_json', 'current_run_ticket'); + if (!row) { + throw new NotFoundError(`Task ${taskId} does not exist`); + } + if (!row.current_run_ticket) { + throw new ConflictError(`Task ${taskId} is not running`); + } + + const settings = taskSettingsV2Schema.parse(JSON.parse(row.settings_json)); + const nextRun = TaskWorker.computeNextRunStartAt(knex, settings); + + const updatedRows = await knex(DB_TASKS_TABLE) + .where('id', '=', taskId) + .where('current_run_ticket', '=', row.current_run_ticket) + .update({ + next_run_start_at: nextRun, + current_run_ticket: knex.raw('null'), + current_run_started_at: knex.raw('null'), + current_run_expires_at: knex.raw('null'), + last_run_ended_at: knex.fn.now(), + last_run_error_json: serializeError(new Error('Task was cancelled')), + }); + if (updatedRows < 1) { + throw new ConflictError(`Task ${taskId} is not running`); + } + } + static async taskStates( knex: Knex, ): Promise> { @@ -227,11 +257,22 @@ export class TaskWorker { } // Abort the task execution either if the worker is stopped, or if the - // task timeout is hit + // task timeout is hit, or if the task ticket was lost (e.g. due to + // cancellation from another host) const taskAbortController = delegateAbortController(signal); const timeoutHandle = setTimeout(() => { taskAbortController.abort(); }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds')); + let livenessHandle: ReturnType | undefined; + const scheduleLivenessCheck = () => { + livenessHandle = setTimeout(async () => { + await this.checkLiveness(ticket, taskAbortController); + if (!taskAbortController.signal.aborted) { + scheduleLivenessCheck(); + } + }, this.workCheckFrequency.as('milliseconds')); + }; + scheduleLivenessCheck(); try { this.#workerState = { @@ -248,6 +289,7 @@ export class TaskWorker { status: 'idle', }; clearTimeout(timeoutHandle); + clearTimeout(livenessHandle); } await this.tryReleaseTask(ticket, taskSettings); @@ -283,7 +325,7 @@ export class TaskWorker { // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same const timeConverted = DateTime.fromJSDate(time.toJSDate()); - nextStartAt = this.nextRunAtRaw(timeConverted); + nextStartAt = TaskWorker.nextRunAtRaw(this.knex, timeConverted); startAt ||= nextStartAt; } else if (isManual) { nextStartAt = this.knex.raw('null'); @@ -334,6 +376,33 @@ export class TaskWorker { ); } + /** + * Checks whether the current task ticket is still valid in the database. + * If the ticket has been cleared (e.g. by cancellation or janitor cleanup), + * aborts the task execution. + */ + private async checkLiveness( + ticket: string, + taskAbortController: AbortController, + ): Promise { + try { + const [row] = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .select('current_run_ticket'); + + if (!row || row.current_run_ticket !== ticket) { + this.logger.info( + `Task ticket for "${this.taskId}" is no longer valid; aborting execution`, + ); + taskAbortController.abort(); + } + } catch (e) { + this.logger.warn( + `Failed to check liveness for task "${this.taskId}", ${e}`, + ); + } + } + /** * Check if the task is ready to run */ @@ -407,48 +476,49 @@ export class TaskWorker { return rows === 1; } + private static computeNextRunStartAt( + knex: Knex, + settings: TaskSettingsV2, + ): Knex.Raw { + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; + + if (isCron) { + const time = new CronTime(settings.cadence).sendAt().toUTC(); + const timeConverted = DateTime.fromJSDate(time.toJSDate()); + return TaskWorker.nextRunAtRaw(knex, timeConverted); + } + + if (isManual) { + return knex.raw('null'); + } + + const dt = Duration.fromISO(settings.cadence).as('seconds'); + + if (knex.client.config.client.includes('sqlite3')) { + return knex.raw(`max(datetime(next_run_start_at, ?), datetime('now'))`, [ + `+${dt} seconds`, + ]); + } + + if (knex.client.config.client.includes('mysql')) { + return knex.raw( + `greatest(next_run_start_at + interval ${dt} second, now())`, + ); + } + + return knex.raw( + `greatest(next_run_start_at + interval '${dt} seconds', now())`, + ); + } + async tryReleaseTask( ticket: string, settings: TaskSettingsV2, error?: Error, ): Promise { - const isManual = settings?.cadence === 'manual'; - const isDuration = settings?.cadence.startsWith('P'); - const isCron = !isManual && !isDuration; - - let nextRun: Knex.Raw; - if (isCron) { - const time = new CronTime(settings.cadence).sendAt().toUTC(); - this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); - // We make a conversion here to make typescript happy, because the luxon versions of the cron library and here may not be the same - const timeConverted = DateTime.fromJSDate(time.toJSDate()); - - nextRun = this.nextRunAtRaw(timeConverted); - } else if (isManual) { - nextRun = this.knex.raw('null'); - } else { - const dt = Duration.fromISO(settings.cadence).as('seconds'); - this.logger.debug( - `task: ${this.taskId} will next occur around ${DateTime.now().plus({ - seconds: dt, - })}`, - ); - - if (this.knex.client.config.client.includes('sqlite3')) { - nextRun = this.knex.raw( - `max(datetime(next_run_start_at, ?), datetime('now'))`, - [`+${dt} seconds`], - ); - } else if (this.knex.client.config.client.includes('mysql')) { - nextRun = this.knex.raw( - `greatest(next_run_start_at + interval ${dt} second, now())`, - ); - } else { - nextRun = this.knex.raw( - `greatest(next_run_start_at + interval '${dt} seconds', now())`, - ); - } - } + const nextRun = TaskWorker.computeNextRunStartAt(this.knex, settings); const rows = await this.knex(DB_TASKS_TABLE) .where('id', '=', this.taskId) @@ -467,12 +537,13 @@ export class TaskWorker { return rows === 1; } - private nextRunAtRaw(time: DateTime): Knex.Raw { - if (this.knex.client.config.client.includes('sqlite3')) { - return this.knex.raw('datetime(?)', [time.toISO()]); - } else if (this.knex.client.config.client.includes('mysql')) { - return this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + private static nextRunAtRaw(knex: Knex, time: DateTime): Knex.Raw { + if (knex.client.config.client.includes('sqlite3')) { + return knex.raw('datetime(?)', [time.toISO()]); } - return this.knex.raw(`?`, [time.toISO()]); + if (knex.client.config.client.includes('mysql')) { + return knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } + return knex.raw(`?`, [time.toISO()]); } } diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts index 186e5f6940..8aacdc0005 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts @@ -18,6 +18,7 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DefaultSchedulerService } from './lib/DefaultSchedulerService'; /** @@ -37,6 +38,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle: coreServices.rootLifecycle, httpRouter: coreServices.httpRouter, pluginMetadata: coreServices.pluginMetadata, + metrics: metricsServiceRef, }, async factory({ database, @@ -44,6 +46,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle, httpRouter, pluginMetadata, + metrics, }) { return DefaultSchedulerService.create({ database, @@ -51,6 +54,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle, httpRouter, pluginMetadata, + metrics, }); }, }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts deleted file mode 100644 index 1a72d2af73..0000000000 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts +++ /dev/null @@ -1,656 +0,0 @@ -/* - * Copyright 2020 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 { ConfigReader } from '@backstage/config'; -import { - BitbucketIntegration, - readBitbucketIntegrationConfig, -} from '@backstage/integration'; -import { - createMockDirectory, - mockServices, - registerMswTestHooks, -} from '@backstage/backend-test-utils'; -import fs from 'fs-extra'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import path from 'node:path'; -import { NotModifiedError } from '@backstage/errors'; -import { BitbucketUrlReader } from './BitbucketUrlReader'; -import { DefaultReadTreeResponseFactory } from './tree'; -import getRawBody from 'raw-body'; -import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api'; - -const logger = mockServices.logger.mock(); - -describe('BitbucketUrlReader.factory', () => { - it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => { - const config = new ConfigReader({ - integrations: { - bitbucket: [], - bitbucketCloud: [ - { - username: 'username', - appPassword: 'password', - }, - ], - bitbucketServer: [ - { - host: 'bitbucket-server.local', - token: 'test-token', - }, - ], - }, - }); - const treeResponseFactory = DefaultReadTreeResponseFactory.create({ - config: config, - }); - - const tuples = BitbucketUrlReader.factory({ - config, - logger, - treeResponseFactory, - }); - - expect(tuples).toHaveLength(0); - }); -}); - -describe('BitbucketUrlReader', () => { - const mockDir = createMockDirectory({ mockOsTmpDir: true }); - - beforeEach(mockDir.clear); - - const treeResponseFactory = DefaultReadTreeResponseFactory.create({ - config: new ConfigReader({}), - }); - - const bitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }), - ), - ), - logger, - { treeResponseFactory }, - ); - - const hostedBitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }), - ), - ), - logger, - { treeResponseFactory }, - ); - - const worker = setupServer(); - registerMswTestHooks(worker); - - describe('readUrl', () => { - it('should be able to readUrl via buffer without ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be able to readUrl using provided token', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('authorization')).toBe( - 'Bearer manual-token', - ); - return res(ctx.status(200), ctx.body('foo')); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { token: 'manual-token' }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be able to readUrl via stream without ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const fromStream = await getRawBody(result.stream!()); - expect(fromStream.toString()).toBe('foo'); - }); - - it('should be able to readUrl with matching ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBe( - 'matching-etag-value', - ); - return res(ctx.status(304)); - }, - ), - ); - - await expect( - bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { etag: 'matching-etag-value' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should be able to readUrl without matching ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBe( - 'previous-etag-value', - ); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'new-etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { etag: 'previous-etag-value' }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - expect(result.etag).toBe('new-etag-value'); - }); - - it('should be able to readUrl via buffer without If-Modified-Since', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ctx.set( - 'Last-Modified', - new Date('2020-01-01T00:00:00Z').toUTCString(), - ), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const buffer = await result.buffer(); - expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z')); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be throw not modified when If-Modified-Since returns a 304', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-Modified-Since')).toBe( - new Date('1999 12 31 23:59:59 GMT').toUTCString(), - ); - return res(ctx.status(304)); - }, - ), - ); - - await expect( - bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should be able to readUrl when If-Modified-Since is before Last-Modified', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-Modified-Since')).toBe( - new Date('1999 12 31 23:59:59 GMT').toUTCString(), - ); - return res( - ctx.status(200), - ctx.set( - 'Last-Modified', - new Date('2020-01-01T00:00:00Z').toUTCString(), - ), - ctx.body('foo'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z')); - }); - }); - - describe('read', () => { - it('rejects unknown targets', async () => { - await expect( - bitbucketProcessor.read('https://not.bitbucket.com/apa'), - ).rejects.toThrow( - 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path', - ); - }); - }); - - describe('readTree', () => { - const repoBuffer = fs.readFileSync( - path.resolve( - __dirname, - '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', - ), - ); - - const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - mainbranch: { - type: 'branch', - name: 'master', - }, - }), - ), - ), - rest.get( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', - ), - ctx.body(new Uint8Array(repoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock.tgz', - ), - ctx.body(new Uint8Array(privateBitbucketRepoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('returns the wanted files from an archive', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(2); - const mkDocsFile = await files[0].content(); - const indexMarkdownFile = await files[1].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - expect(mkDocsFile.toString()).toBe('site_name: Test\n'); - }); - - it('creates a directory with the wanted files', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - ); - - const dir = await response.dir({ targetDir: mockDir.path }); - - await expect( - fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), - ).resolves.toBe('site_name: Test\n'); - await expect( - fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), - ).resolves.toBe('# Test\n'); - }); - - it('uses private bitbucket host', async () => { - const response = await hostedBitbucketProcessor.readTree( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(1); - const indexMarkdownFile = await files[0].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - }); - - it('returns the wanted files from an archive with a subpath', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master/docs', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(1); - const indexMarkdownFile = await files[0].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - }); - - it('creates a directory with the wanted files with a subpath', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master/docs', - ); - - const dir = await response.dir({ targetDir: mockDir.path }); - - await expect( - fs.readFile(path.join(dir, 'index.md'), 'utf8'), - ).resolves.toBe('# Test\n'); - }); - - it('throws a NotModifiedError when given a etag in options', async () => { - const fnBitbucket = async () => { - await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - { etag: '12ab34cd56ef' }, - ); - }; - - await expect(fnBitbucket).rejects.toThrow(NotModifiedError); - }); - - it('should not throw a NotModifiedError when given an outdated etag in options', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - { etag: 'outdatedetag123abc' }, - ); - - expect(response.etag).toBe('12ab34cd56ef'); - }); - }); - - describe('search hosted', () => { - const repoBuffer = fs.readFileSync( - path.resolve( - __dirname, - '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', - ), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - mainbranch: { - type: 'branch', - name: 'master', - }, - }), - ), - ), - rest.get( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', - ), - ctx.body(new Uint8Array(repoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('works for the naive case', async () => { - const result = await bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/**/index.*', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('works in nested folders', async () => { - const result = await bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.*', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('throws NotModifiedError when same etag', async () => { - await expect( - bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/**/index.*', - { etag: '12ab34cd56ef' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - }); - - describe('search private', () => { - const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock.tgz', - ), - ctx.body(new Uint8Array(privateBitbucketRepoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('works for the naive case', async () => { - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('works in nested folders', async () => { - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('throws NotModifiedError when same etag', async () => { - await expect( - hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', - { etag: '12ab34cd56ef' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should work for exact URLs', async () => { - hostedBitbucketProcessor.readUrl = jest.fn().mockResolvedValue({ - buffer: async () => Buffer.from('content'), - etag: 'etag', - } as UrlReaderServiceReadUrlResponse); - - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - expect(result.etag).toBe('etag'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - expect((await result.files[0].content()).toString()).toEqual('content'); - }); - }); -}); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts deleted file mode 100644 index 8c8843d7e9..0000000000 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright 2020 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 { - UrlReaderService, - UrlReaderServiceReadTreeOptions, - UrlReaderServiceReadTreeResponse, - UrlReaderServiceReadUrlOptions, - UrlReaderServiceReadUrlResponse, - UrlReaderServiceSearchOptions, - UrlReaderServiceSearchResponse, -} from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; -import { - BitbucketIntegration, - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, - ScmIntegrations, -} from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; -import { trimEnd } from 'lodash'; -import { Minimatch } from 'minimatch'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { ReaderFactory, ReadTreeResponseFactory } from './types'; -import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; - -/** - * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for files from Bitbucket v1 and v2 APIs, such - * as the one exposed by Bitbucket Cloud itself. - * - * @public - * @deprecated in favor of BitbucketCloudUrlReader and BitbucketServerUrlReader - */ -export class BitbucketUrlReader implements UrlReaderService { - static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => { - const integrations = ScmIntegrations.fromConfig(config); - return integrations.bitbucket - .list() - .filter( - item => - !integrations.bitbucketCloud.byHost(item.config.host) && - !integrations.bitbucketServer.byHost(item.config.host), - ) - .map(integration => { - const reader = new BitbucketUrlReader(integration, logger, { - treeResponseFactory, - }); - const predicate = (url: URL) => url.host === integration.config.host; - return { reader, predicate }; - }); - }; - - private readonly integration: BitbucketIntegration; - private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }; - - constructor( - integration: BitbucketIntegration, - logger: LoggerService, - deps: { treeResponseFactory: ReadTreeResponseFactory }, - ) { - this.integration = integration; - this.deps = deps; - const { host, token, username, appPassword } = integration.config; - const replacement = - host === 'bitbucket.org' ? 'bitbucketCloud' : 'bitbucketServer'; - logger.warn( - `[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`, - ); - - if (!token && username && !appPassword) { - throw new Error( - `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, - ); - } - } - - async read(url: string): Promise { - const response = await this.readUrl(url); - return response.buffer(); - } - - private getCredentials = async (options?: { - token?: string; - }): Promise<{ headers: Record }> => { - if (options?.token) { - return { - headers: { - Authorization: `Bearer ${options.token}`, - }, - }; - } - - return await getBitbucketRequestOptions(this.integration.config); - }; - - async readUrl( - url: string, - options?: UrlReaderServiceReadUrlOptions, - ): Promise { - const { etag, lastModifiedAfter, signal } = options ?? {}; - const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); - const requestOptions = await this.getCredentials(options); - - let response: Response; - try { - response = await fetch(bitbucketUrl.toString(), { - headers: { - ...requestOptions.headers, - ...(etag && { 'If-None-Match': etag }), - ...(lastModifiedAfter && { - 'If-Modified-Since': lastModifiedAfter.toUTCString(), - }), - }, - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can be - // removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - ...(signal && { signal: signal as any }), - }); - } catch (e) { - throw new Error(`Unable to read ${url}, ${e}`); - } - - if (response.status === 304) { - throw new NotModifiedError(); - } - - if (response.ok) { - return ReadUrlResponseFactory.fromResponse(response); - } - - const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - async readTree( - url: string, - options?: UrlReaderServiceReadTreeOptions, - ): Promise { - const { filepath } = parseGitUrl(url); - - const lastCommitShortHash = await this.getLastCommitShortHash(url); - if (options?.etag && options.etag === lastCommitShortHash) { - throw new NotModifiedError(); - } - - const downloadUrl = await getBitbucketDownloadUrl( - url, - this.integration.config, - ); - const archiveBitbucketResponse = await fetch( - downloadUrl, - getBitbucketRequestOptions(this.integration.config), - ); - if (!archiveBitbucketResponse.ok) { - const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`; - if (archiveBitbucketResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - return await this.deps.treeResponseFactory.fromTarArchive({ - response: archiveBitbucketResponse, - subpath: filepath, - etag: lastCommitShortHash, - filter: options?.filter, - }); - } - - async search( - url: string, - options?: UrlReaderServiceSearchOptions, - ): Promise { - const { filepath } = parseGitUrl(url); - - // If it's a direct URL we use readUrl instead - if (!filepath?.match(/[*?]/)) { - try { - const data = await this.readUrl(url, options); - - return { - files: [ - { - url: url, - content: data.buffer, - lastModifiedAt: data.lastModifiedAt, - }, - ], - etag: data.etag ?? '', - }; - } catch (error) { - assertError(error); - if (error.name === 'NotFoundError') { - return { - files: [], - etag: '', - }; - } - throw error; - } - } - - const matcher = new Minimatch(filepath); - - // TODO(freben): For now, read the entire repo and filter through that. In - // a future improvement, we could be smart and try to deduce that non-glob - // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used - // to get just that part of the repo. - const treeUrl = trimEnd(url.replace(filepath, ''), '/'); - - const tree = await this.readTree(treeUrl, { - etag: options?.etag, - filter: path => matcher.match(path), - }); - const files = await tree.files(); - - return { - etag: tree.etag, - files: files.map(file => ({ - url: this.integration.resolveUrl({ - url: `/${file.path}`, - base: url, - }), - content: file.content, - lastModifiedAt: file.lastModifiedAt, - })), - }; - } - - toString() { - const { host, token, username, appPassword } = this.integration.config; - let authed = Boolean(token); - if (!authed) { - authed = Boolean(username && appPassword); - } - return `bitbucket{host=${host},authed=${authed}}`; - } - - private async getLastCommitShortHash(url: string): Promise { - const { resource, name: repoName, owner: project, ref } = parseGitUrl(url); - - let branch = ref; - if (!branch) { - branch = await getBitbucketDefaultBranch(url, this.integration.config); - } - - const isHosted = resource === 'bitbucket.org'; - // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 - const commitsApiUrl = isHosted - ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` - : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; - - const commitsResponse = await fetch( - commitsApiUrl, - getBitbucketRequestOptions(this.integration.config), - ); - if (!commitsResponse.ok) { - const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; - if (commitsResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - const commits = await commitsResponse.json(); - if (isHosted) { - if ( - commits && - commits.values && - commits.values.length > 0 && - commits.values[0].hash - ) { - return commits.values[0].hash.substring(0, 12); - } - } else { - if ( - commits && - commits.values && - commits.values.length > 0 && - commits.values[0].id - ) { - return commits.values[0].id.substring(0, 12); - } - } - - throw new Error(`Failed to read response from ${commitsApiUrl}`); - } -} diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index 4bae21a3ef..d7513a22d6 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -78,9 +78,6 @@ describe('GitlabUrlReader', () => { describe('read', () => { beforeEach(() => { worker.use( - rest.get('*/api/v4/projects/:name', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), rest.get('*', (req, res, ctx) => res( ctx.status(200), @@ -107,14 +104,14 @@ describe('GitlabUrlReader', () => { url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', config: createConfig(), response: expect.objectContaining({ - url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FteamA%2FsubgroupA%2FrepoA/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', }), }, { url: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', config: createConfig('0123456789'), response: expect.objectContaining({ - url: 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.example.com/api/v4/projects/groupA%2Fteams%2FteamA%2FsubgroupA%2FrepoA/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', headers: expect.objectContaining({ authorization: 'Bearer 0123456789', }), @@ -124,7 +121,7 @@ describe('GitlabUrlReader', () => { url: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup config: createConfig(), response: expect.objectContaining({ - url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + url: 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FteamA%2FrepoA/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', }), }, @@ -133,7 +130,7 @@ describe('GitlabUrlReader', () => { url: 'https://gitlab.example.com/a/b/blob/master/c.yaml', config: createConfig(), response: expect.objectContaining({ - url: 'https://gitlab.example.com/api/v4/projects/12345/repository/files/c.yaml/raw?ref=master', + url: 'https://gitlab.example.com/api/v4/projects/a%2Fb/repository/files/c.yaml/raw?ref=master', }), }, ])('should handle happy path %#', async ({ url, config, response }) => { @@ -177,9 +174,6 @@ describe('GitlabUrlReader', () => { it('should throw NotModified on HTTP 304 from etag', async () => { worker.use( - rest.get('*/api/v4/projects/:name', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), rest.get('*', (req, res, ctx) => { expect(req.headers.get('If-None-Match')).toBe('999'); return res(ctx.status(304)); @@ -198,9 +192,6 @@ describe('GitlabUrlReader', () => { it('should throw NotModified on HTTP 304 from lastModifiedAt', async () => { worker.use( - rest.get('*/api/v4/projects/:name', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), rest.get('*', (req, res, ctx) => { expect(req.headers.get('If-Modified-Since')).toBe( new Date('2019 12 31 23:59:59 GMT').toUTCString(), @@ -221,9 +212,6 @@ describe('GitlabUrlReader', () => { it('should return etag and last-modified in response', async () => { worker.use( - rest.get('*/api/v4/projects/:name', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), rest.get('*', (_req, res, ctx) => { return res( ctx.status(200), @@ -248,15 +236,6 @@ describe('GitlabUrlReader', () => { it('should return the file when using a user token', async () => { worker.use( - rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-user-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), rest.get('*', (_req, res, ctx) => { return res(ctx.status(200), ctx.body('foo')); }), @@ -566,18 +545,6 @@ describe('GitlabUrlReader', () => { }); it('should return the file when using a user token', async () => { - worker.use( - rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-user-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), - ); - const response = await gitlabProcessor.readTree( 'https://gitlab.com/user/project/tree/main', { token: 'gl-user-token' }, @@ -738,30 +705,13 @@ describe('GitlabUrlReader', () => { }); describe('getGitlabFetchUrl', () => { - beforeEach(() => { - worker.use( - rest.get( - '*/api/v4/projects/group%2Fsubgroup%2Fproject', - (_, res, ctx) => res(ctx.status(200), ctx.json({ id: 12345 })), - ), - rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-user-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), - ); - }); it('should fall back to getGitLabFileFetchUrl for blob urls', async () => { await expect( (gitlabProcessor as any).getGitlabFetchUrl( 'https://gitlab.com/group/subgroup/project/-/blob/branch/my/path/to/file.yaml', ), ).resolves.toEqual( - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + 'https://gitlab.com/api/v4/projects/group%2Fsubgroup%2Fproject/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', ); }); it('should work for job artifact urls', async () => { @@ -770,7 +720,7 @@ describe('GitlabUrlReader', () => { 'https://gitlab.com/group/subgroup/project/-/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', ), ).resolves.toEqual( - 'https://gitlab.com/api/v4/projects/12345/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', + 'https://gitlab.com/api/v4/projects/group%2Fsubgroup%2Fproject/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', ); }); it('should fail on unfamiliar or non-Gitlab urls', async () => { @@ -779,7 +729,7 @@ describe('GitlabUrlReader', () => { 'https://gitlab.com/some/random/endpoint', ), ).rejects.toThrow( - 'Failed converting /some/random/endpoint to a project id. Url path must include /blob/.', + 'Failed extracting project path from /some/random/endpoint. Url path must include /blob/.', ); }); it('should resolve the project path using a user token', async () => { @@ -789,39 +739,18 @@ describe('GitlabUrlReader', () => { 'gl-user-token', ), ).resolves.toEqual( - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + 'https://gitlab.com/api/v4/projects/user%2Fproject/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', ); }); }); describe('getGitlabArtifactFetchUrl', () => { - beforeEach(() => { - worker.use( - rest.get( - '*/api/v4/projects/group%2Fsubgroup%2Fproject', - (_, res, ctx) => res(ctx.status(200), ctx.json({ id: 12345 })), - ), - rest.get( - '*/api/v4/projects/groupA%2Fsubgroup%2Fproject', - (_, res, ctx) => res(ctx.status(404)), - ), - rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-user-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), - ); - }); - it('should reject urls that are not for the job artifacts API', async () => { - await expect( + it('should reject urls that are not for the job artifacts API', () => { + expect(() => (gitlabProcessor as any).getGitlabArtifactFetchUrl( new URL('https://gitlab.com/some/url'), ), - ).rejects.toThrow('Unable to process url as an GitLab artifact'); + ).toThrow('Unable to process url as an GitLab artifact'); }); it('should work for job artifact urls', async () => { await expect( @@ -832,18 +761,22 @@ describe('GitlabUrlReader', () => { ), ).resolves.toEqual( new URL( - 'https://gitlab.com/api/v4/projects/12345/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', + 'https://gitlab.com/api/v4/projects/group%2Fsubgroup%2Fproject/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', ), ); }); - it('errors in mapping the project ID should be captured', async () => { + it('should work for job artifact urls with any project path', async () => { await expect( (gitlabProcessor as any).getGitlabArtifactFetchUrl( new URL( 'https://gitlab.com/groupA/subgroup/project/-/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', ), ), - ).rejects.toThrow(/^Unable to translate GitLab artifact URL:/); + ).resolves.toEqual( + new URL( + 'https://gitlab.com/api/v4/projects/groupA%2Fsubgroup%2Fproject/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', + ), + ); }); it('should resolve the project path using a user token', async () => { await expect( @@ -855,51 +788,9 @@ describe('GitlabUrlReader', () => { ), ).resolves.toEqual( new URL( - 'https://gitlab.com/api/v4/projects/12345/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', + 'https://gitlab.com/api/v4/projects/user%2Fproject/jobs/artifacts/branch/raw/my/path/to/file.yaml?job=myJob', ), ); }); }); - - describe('resolveProjectToId', () => { - beforeEach(() => { - worker.use( - rest.get('*/api/v4/projects/group%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-dummy-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), - rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('authorization') !== 'Bearer gl-user-token') { - return res( - ctx.status(401), - ctx.json({ message: '401 Unauthorized' }), - ); - } - return res(ctx.status(200), ctx.json({ id: 12345 })); - }), - ); - }); - - it('should resolve the project path to a valid project id', async () => { - await expect( - (gitlabProcessor as any).resolveProjectToId( - new URL('https://gitlab.com/group/project'), - ), - ).resolves.toEqual(12345); - }); - - it('should resolve the project path to a valid project id using a user token', async () => { - await expect( - (gitlabProcessor as any).resolveProjectToId( - new URL('https://gitlab.com/user/project'), - 'gl-user-token', - ), - ).resolves.toEqual(12345); - }); - }); }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index 182dc2cf28..a85b9fbb89 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -// NOTE(freben): Intentionally uses node-fetch because of https://github.com/backstage/backstage/issues/28190 -import fetch, { Response } from 'node-fetch'; - import { UrlReaderService, UrlReaderServiceReadTreeOptions, @@ -41,10 +38,8 @@ import { import parseGitUrl from 'git-url-parse'; import { trimEnd, trimStart } from 'lodash'; import { Minimatch } from 'minimatch'; -import { Readable } from 'node:stream'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import { ReaderFactory, ReadTreeResponseFactory } from './types'; -import { parseLastModified } from './util'; /** * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for files on GitLab. @@ -89,7 +84,7 @@ export class GitlabUrlReader implements UrlReaderService { let response: Response; try { - response = await fetch(builtUrl, { + response = await this.integration.fetch(builtUrl, { headers: { ...getGitLabRequestOptions(this.integration.config, token).headers, ...(etag && !isArtifact && { 'If-None-Match': etag }), @@ -115,12 +110,7 @@ export class GitlabUrlReader implements UrlReaderService { } if (response.ok) { - return ReadUrlResponseFactory.fromNodeJSReadable(response.body, { - etag: response.headers.get('ETag') ?? undefined, - lastModifiedAt: parseLastModified( - response.headers.get('Last-Modified'), - ), - }); + return ReadUrlResponseFactory.fromResponse(response); } const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; @@ -155,7 +145,7 @@ export class GitlabUrlReader implements UrlReaderService { // Use GitLab API to get the default branch // encodeURIComponent is required for GitLab API // https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding - const projectGitlabResponse = await fetch( + const projectGitlabResponse = await this.integration.fetch( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( repoFullName, @@ -182,7 +172,7 @@ export class GitlabUrlReader implements UrlReaderService { if (!!filepath) { commitsReqParams.set('path', filepath); } - const commitsGitlabResponse = await fetch( + const commitsGitlabResponse = await this.integration.fetch( new URL( `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( repoFullName, @@ -218,23 +208,23 @@ export class GitlabUrlReader implements UrlReaderService { archiveReqParams.set('path', filepath); } // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive - const archiveGitLabResponse = await fetch( - `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( - repoFullName, - )}/repository/archive?${archiveReqParams.toString()}`, - { - ...getGitLabRequestOptions(this.integration.config, token), - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can - // be removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - ...(signal && { signal: signal as any }), - }, - ); + const reqUrl = `${ + this.integration.config.apiBaseUrl + }/projects/${encodeURIComponent( + repoFullName, + )}/repository/archive?${archiveReqParams.toString()}`; + const archiveGitLabResponse = await this.integration.fetch(reqUrl, { + ...getGitLabRequestOptions(this.integration.config, token), + // TODO(freben): The signal cast is there because pre-3.x versions of + // node-fetch have a very slightly deviating AbortSignal type signature. + // The difference does not affect us in practice however. The cast can + // be removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + ...(signal && { signal: signal as any }), + }); if (!archiveGitLabResponse.ok) { - const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; + const message = `Failed to read tree (archive) from ${url}, ${reqUrl}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; if (archiveGitLabResponse.status === 404) { throw new NotFoundError(message); } @@ -242,7 +232,7 @@ export class GitlabUrlReader implements UrlReaderService { } return await this.deps.treeResponseFactory.fromTarArchive({ - stream: Readable.from(archiveGitLabResponse.body), + response: archiveGitLabResponse, subpath: filepath, etag: commitSha, filter: options?.filter, @@ -337,74 +327,48 @@ export class GitlabUrlReader implements UrlReaderService { // If the target is for a job artifact then go down that path const targetUrl = new URL(target); if (targetUrl.pathname.includes('/-/jobs/artifacts/')) { - return this.getGitlabArtifactFetchUrl(targetUrl, token).then(value => + return this.getGitlabArtifactFetchUrl(targetUrl).then(value => value.toString(), ); } - // Default to the old behavior of assuming the url is for a file + // Default to the optimized behavior - no API call needed for file URLs return getGitLabFileFetchUrl(target, this.integration.config, token); } // convert urls of the form: // https://example.com///-/jobs/artifacts//raw/?job= // to urls of the form: - // https://example.com/api/v4/projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job= - private async getGitlabArtifactFetchUrl( - target: URL, - token?: string, - ): Promise { + // https://example.com/api/v4/projects/namespace%2Fproject/jobs/artifacts/:ref_name/raw/*artifact_path?job= + private getGitlabArtifactFetchUrl(target: URL): Promise { if (!target.pathname.includes('/-/jobs/artifacts/')) { throw new Error('Unable to process url as an GitLab artifact'); } try { const [namespaceAndProject, ref] = target.pathname.split('/-/jobs/artifacts/'); - const projectPath = new URL(target); - projectPath.pathname = namespaceAndProject; - const projectId = await this.resolveProjectToId(projectPath, token); + + // Extract project path directly instead of making API call const relativePath = getGitLabIntegrationRelativePath( this.integration.config, ); + + let projectPath = namespaceAndProject; + // Check relative path exist and remove it + if (relativePath) { + projectPath = trimStart(projectPath, relativePath); + } + // Trim an initial / if it exists + projectPath = projectPath.replace(/^\//, ''); + const newUrl = new URL(target); - newUrl.pathname = `${relativePath}/api/v4/projects/${projectId}/jobs/artifacts/${ref}`; - return newUrl; + newUrl.pathname = `${relativePath}/api/v4/projects/${encodeURIComponent( + projectPath, + )}/jobs/artifacts/${ref}`; + return Promise.resolve(newUrl); } catch (e) { throw new Error( `Unable to translate GitLab artifact URL: ${target}, ${e}`, ); } } - - private async resolveProjectToId( - pathToProject: URL, - token?: string, - ): Promise { - let project = pathToProject.pathname; - // Check relative path exist and remove it if so - const relativePath = getGitLabIntegrationRelativePath( - this.integration.config, - ); - if (relativePath) { - project = project.replace(relativePath, ''); - } - // Trim an initial / if it exists - project = project.replace(/^\//, ''); - const result = await fetch( - `${ - pathToProject.origin - }${relativePath}/api/v4/projects/${encodeURIComponent(project)}`, - getGitLabRequestOptions(this.integration.config, token), - ); - const data = await result.json(); - if (!result.ok) { - if (result.status === 401) { - throw new Error( - 'GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project', - ); - } - - throw new Error(`Gitlab error: ${data.error}, ${data.error_description}`); - } - return Number(data.id); - } } diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts index 229bdd4903..f04a668b0a 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts @@ -24,7 +24,6 @@ import { UrlReaderPredicateMux } from './UrlReaderPredicateMux'; import { AzureUrlReader } from './AzureUrlReader'; import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; import { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; -import { BitbucketUrlReader } from './BitbucketUrlReader'; import { GerritUrlReader } from './GerritUrlReader'; import { GithubUrlReader } from './GithubUrlReader'; import { GitlabUrlReader } from './GitlabUrlReader'; @@ -92,7 +91,6 @@ export class UrlReaders { AzureUrlReader.factory, BitbucketCloudUrlReader.factory, BitbucketServerUrlReader.factory, - BitbucketUrlReader.factory, GerritUrlReader.factory, GithubUrlReader.factory, GiteaUrlReader.factory, diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts index fc33efbc3a..3f5d7a7ea5 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts @@ -16,7 +16,6 @@ export { AzureUrlReader } from './AzureUrlReader'; export { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; -export { BitbucketUrlReader } from './BitbucketUrlReader'; export { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; export { GerritUrlReader } from './GerritUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; diff --git a/packages/backend-dev-utils/CHANGELOG.md b/packages/backend-dev-utils/CHANGELOG.md index 89cde5dcf9..27be4749ca 100644 --- a/packages/backend-dev-utils/CHANGELOG.md +++ b/packages/backend-dev-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-dev-utils +## 0.1.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + ## 0.1.7-next.0 ### Patch Changes diff --git a/packages/backend-dev-utils/package.json b/packages/backend-dev-utils/package.json index 3cfec3f23c..4da795131a 100644 --- a/packages/backend-dev-utils/package.json +++ b/packages/backend-dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dev-utils", - "version": "0.1.7-next.0", + "version": "0.1.7", "backstage": { "role": "node-library" }, diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index d834e0fa93..edf0063831 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/backend-dynamic-feature-service +## 0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.43-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-backend@0.6.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.8.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.7.9 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. +- 9b4c414: Updated README for backend-dynamic-feature-service +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/config-loader@1.10.8 + - @backstage/plugin-events-backend@0.5.11 + - @backstage/plugin-search-common@1.2.22 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-app-node@0.1.42 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + ## 0.7.9-next.1 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/README.md b/packages/backend-dynamic-feature-service/README.md index 8122810f96..914df15f65 100644 --- a/packages/backend-dynamic-feature-service/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -97,7 +97,7 @@ Since this service only handles loading, you would choose a packaging approach b **When to use:** Plugin only uses dependencies that are already provided by the main Backstage application. -**How to apply:** +**How to use:** ```bash cd my-backstage-plugin @@ -117,7 +117,7 @@ tar -xzf package.tgz -C /path/to/dynamic-plugins-root/my-backstage-plugin --stri **When to use:** Plugin has private dependencies not available in the main Backstage application. -**How to apply:** +**How to use:** ```bash # Package the plugin @@ -136,35 +136,47 @@ yarn install # Installs all the plugin's dependencies **Example scenario:** Plugin needs `axios@1.4.0` which isn't available in the main application. -### 3. Custom packaging CLI tool +### 3. Dedicated bundling CLI command -**When to use:** When you want to produce self-contained dynamic plugin packages that can be directly extracted without any post-action, and systematically use the core `@backstage` dependencies provided by the Backstage application. +**When to use:** -**What a packaging CLI needs to do:** +- When you want to produce self-contained dynamic plugin packages that can be directly extracted and loaded without any post-action, +- especially when your plugin depends on other packages in the same monorepo. -1. **Analyze plugin dependencies** - Identify which are Backstage core vs private dependencies -2. **Create distribution package** - Generate a new directory with modified structure: - - Move `@backstage/*` packages from `dependencies` to `peerDependencies` in package.json - - Keep only private dependencies in the `dependencies` section - - Keep the built JavaScript code unchanged - - Include only the filtered private dependencies in `node_modules` -3. **Result** - A self-contained package that uses the main app's `@backstage/*` packages but includes its own private dependencies +**How to use:** -**Benefits:** - -- Systematic use of main application's `@backstage/*` packages (no version conflicts), enabling the future implementation of `@backstage` dependency version checking at start time -- Self-contained packages with only necessary private dependencies -- No post-installation steps required (extract and run) -- Consistent dependency structure across all dynamic plugins -- Production-ready distribution format - -**Example implementation:** The [`@red-hat-developer-hub/cli`](https://github.com/redhat-developer/rhdh-cli) tool implements this approach: +The [`backstage-cli package bundle`](../cli/cli-report.md) command automates the required steps. Run it from within a plugin directory: ```bash cd my-backstage-plugin -npx @red-hat-developer-hub/cli@latest plugin export -# Creates a self-contained package with embedded dependencies in the `/dist-dynamic` sub-folder - -# Deploy the generated package -cp -r dist-dynamic /path/to/dynamic-plugins-root/my-backstage-plugin +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 ``` + +**Batch bundling:** When bundling many plugins from the same monorepo, use `--pre-packed-dir` to avoid redundant work: + +```bash +# First, build a shared dist workspace +backstage-cli build-workspace dist-workspace --alwaysPack ...plugin-packages + +# Then bundle each plugin using the pre-packed output +cd plugins/my-backstage-plugin +backstage-cli package bundle --pre-packed-dir ../../dist-workspace +``` + +See the full list of options in the [CLI reference](../cli/cli-report.md). + +**What the command does:** + +1. **Builds the plugin** — Produces CJS output for the backend plugin and its transitively-required monorepo packages (`*-node` or `*-common`) +2. **Packs local packages** — Resolves `workspace:^` and `backstage:^` dependencies on both the main plugin package and its transitively-required monorepo packages (`*-node` or `*-common`) +3. **Installs private dependencies** — Seeds a lockfile from the plugin source monorepo and prunes it, then installs a private `node_modules` containing all required dependencies +4. **Collects configuration schemas** — Gathers plugin config schemas and writes them to `dist/.config-schema.json` so they are available for validation at load time + +**Benefits:** + +- Self-contained packages with all necessary dependencies +- No post-installation steps required (extract and run) +- Consistent dependency structure across all dynamic plugins +- Automatic configuration schema collection +- Production-ready distribution format diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 4928ee4148..1891c17498 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.9-next.1", + "version": "0.8.0-next.2", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/dist/index.cjs.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/dist/index.cjs.js new file mode 100644 index 0000000000..f3671bc0a7 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/dist/index.cjs.js @@ -0,0 +1,28 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +// This require resolves to the bundled proxy copy inside this plugin's +// own node_modules/@backstage/backend-plugin-api, NOT the host's copy. +var backendPluginApi = require('@backstage/backend-plugin-api'); + +// Triggers the _resolveFilename fallback: require.resolve runs from +// the bundled @backstage/backend-plugin-api whose mod.path is inside +// this plugin's node_modules. +var pkgDir = backendPluginApi.resolvePackagePath('plugin-test-backend-bundled'); + +const testBundledPlugin = backendPluginApi.createBackendPlugin({ + pluginId: "test-bundled", + register(env) { + env.registerInit({ + deps: { + logger: backendPluginApi.coreServices.rootLogger, + }, + async init({ logger }) { + logger.info("Bundled backend plugin loaded successfully"); + } + }); + } +}); + +exports.default = testBundledPlugin; diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/index.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/index.js new file mode 100644 index 0000000000..70e57950be --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/index.js @@ -0,0 +1,22 @@ +'use strict'; + +// Proxy that simulates a bundled copy of @backstage/backend-plugin-api. +// Re-exports everything from the real workspace package, but provides a +// local resolvePackagePath so that require.resolve() runs from THIS +// file's context (mod.path inside node_modules/@backstage/backend-plugin-api). + +const nodePath = require('node:path'); +const real = require('../../../../../../../../../backend-plugin-api/src'); + +function resolvePackagePath(name) { + const args = Array.prototype.slice.call(arguments, 1); + const pkgJson = require.resolve(name + '/package.json'); + return nodePath.resolve.apply( + nodePath, + [nodePath.dirname(pkgJson)].concat(args), + ); +} + +module.exports = Object.assign({}, real, { + resolvePackagePath: resolvePackagePath, +}); diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/package.json new file mode 100644 index 0000000000..f487169afe --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/node_modules/@backstage/backend-plugin-api/package.json @@ -0,0 +1,6 @@ +{ + "name": "@backstage/backend-plugin-api", + "version": "0.0.0", + "description": "Proxy that re-exports the real @backstage/backend-plugin-api with a local resolvePackagePath, simulating a bundled copy.", + "main": "index.js" +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/package.json new file mode 100644 index 0000000000..bb2dbf15a6 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root-for-bundled/test-backend-bundled-dynamic/package.json @@ -0,0 +1,31 @@ +{ + "name": "plugin-test-backend-bundled-dynamic", + "version": "0.0.0", + "description": "A test dynamic backend plugin that bundles its own @backstage/backend-plugin-api.", + "backstage": { + "role": "backend-plugin", + "pluginId": "test-bundled", + "pluginPackages": [ + "plugin-test-backend-bundled" + ] + }, + "keywords": [ + "backstage", + "dynamic" + ], + "exports": { + ".": { + "require": "./dist/index.cjs.js", + "default": "./dist/index.cjs.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs.js", + "files": [ + "dist" + ], + "dependencies": { + "@backstage/backend-plugin-api": "0.0.0" + }, + "bundleDependencies": true +} diff --git a/packages/backend-dynamic-feature-service/src/features/features.test.ts b/packages/backend-dynamic-feature-service/src/features/features.test.ts index ce15964c77..ac6c6ea510 100644 --- a/packages/backend-dynamic-feature-service/src/features/features.test.ts +++ b/packages/backend-dynamic-feature-service/src/features/features.test.ts @@ -465,6 +465,45 @@ Require stack: }); }); + it('should load a backend plugin that bundles its own @backstage/backend-plugin-api', async () => { + const dynamicPluginsLister = new DynamicPluginLister(); + const dynamicPluginsRootForBundled = resolvePath( + __dirname, + '__fixtures__/dynamic-plugins-root-for-bundled', + ); + await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + dynamicPlugins: { + rootDirectory: dynamicPluginsRootForBundled, + }, + backend: { + baseUrl: `http://localhost:0`, + }, + }, + }), + dynamicPluginsFeatureLoader({ + moduleLoader: logger => + jestFreeTypescriptAwareModuleLoader({ logger }), + }), + dynamicPluginsLister.feature(), + ], + }); + + expect(dynamicPluginsLister.loadedPlugins).toMatchObject([ + { + installer: { + kind: 'new', + }, + name: 'plugin-test-backend-bundled-dynamic', + platform: 'node', + role: 'backend-plugin', + version: '0.0.0', + }, + ]); + }); + describe('module federation support', () => { const createRemoteProviderPlugin = ( provider: FrontendRemoteResolverProvider, diff --git a/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts index 3cc640086f..8902decdfa 100644 --- a/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts +++ b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts @@ -99,15 +99,21 @@ export class CommonJSModuleLoader implements ModuleLoader { ); } - // Are we trying to resolve a `package.json` from an originating module of the core backstage application - // (this is mostly done by calling `@backstage/backend-plugin-api/resolvePackagePath`). - const resolvingPackageJsonFromBackstageApplication = + // Is this a `resolvePackagePath` call from `@backstage/backend-plugin-api`? + // This covers both the host application's copy and a bundled copy living + // inside a dynamic plugin's own node_modules. + // The regex matches mod.path against the various ways the package can be resolved on disk + // (with optional subdirectory such as /src or /dist after the package name): + // - .../node_modules/@backstage/backend-plugin-api[/...] (npm-installed) + // - ...//node_modules/@backstage/backend-plugin-api[/...] (bundled) + // - .../packages/backend-plugin-api[/...] (symlinked workspace in monorepo) + const resolvingPackageJsonViaResolvePackagePath = request?.endsWith('/package.json') && - mod?.path && - !dynamicPluginsPaths.some(p => mod.path.startsWith(p)); + /[/\\](?:@backstage|packages)[/\\]backend-plugin-api(?:[/\\]|$)/.test( + mod?.path ?? '', + ); - // If not, we don't need the dedicated specific case below. - if (!resolvingPackageJsonFromBackstageApplication) { + if (!resolvingPackageJsonViaResolvePackagePath) { throw errorToThrow; } diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 1f8fa2cfd6..aee492b1b7 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -38,7 +38,7 @@ import { createSpecializedBackend } from '@backstage/backend-app-api'; import { ConfigSources } from '@backstage/config-loader'; import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils'; import { PluginScanner } from '../scanner/plugin-scanner'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { BackstagePackageJson, PackageRole } from '@backstage/cli-node'; @@ -956,7 +956,7 @@ describe('backend-dynamic-feature-service', () => { mockDir.setContent({ 'package.json': fs.readFileSync( - findPaths(__dirname).resolveTargetRoot('package.json'), + targetPaths.resolveRoot('package.json'), ), 'dynamic-plugins-root': {}, 'dynamic-plugins-root/a-dynamic-plugin': ctx => @@ -1044,7 +1044,7 @@ describe('backend-dynamic-feature-service', () => { otherMockDir.resolve('a-dynamic-plugin'), ); expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith( - findPaths(__dirname).targetRoot, + targetPaths.rootDir, [realPath], new Map([ [ diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index c4cb33892d..b79a796eb9 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -35,7 +35,7 @@ import { createServiceRef, } from '@backstage/backend-plugin-api'; import { PackageRole, PackageRoles } from '@backstage/cli-node'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import * as fs from 'node:fs'; /** @@ -56,7 +56,7 @@ export class DynamicPluginManager implements DynamicPluginProvider { options: DynamicPluginManagerOptions, ): Promise { /* eslint-disable-next-line no-restricted-syntax */ - const backstageRoot = findPaths(__dirname).targetRoot; + const backstageRoot = targetPaths.rootDir; const scanner = PluginScanner.create({ config: options.config, logger: options.logger, diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi.yaml b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml index 9b1871c366..987b59f4b5 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi.yaml +++ b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: .backstage/dynamic-features version: '1' diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts index 285345d44c..7d241760dc 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts index 8d81cbaf39..9a0ffed740 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts index dec4b8804e..58fc52487a 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts index 7440110ad8..ff5cae705f 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts index 72edba90d4..6e8bdeff0a 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts index 0c54a6585b..4cf296a0b9 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts index cf48bcfc95..b228198c3c 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: '.backstage/dynamic-features', version: '1', diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts index d50742f8fb..400f57829a 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts @@ -20,7 +20,7 @@ import { createServiceFactory, createServiceRef, } from '@backstage/backend-plugin-api'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; import * as path from 'node:path'; @@ -100,7 +100,7 @@ const dynamicPluginsSchemasServiceFactoryWithOptions = ( config, logger, // eslint-disable-next-line no-restricted-syntax - backstageRoot: findPaths(__dirname).targetRoot, + backstageRoot: targetPaths.rootDir, preferAlpha: true, }); diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index db053f579b..af3c24c0bf 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/backend-openapi-utils +## 0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.6 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 0.6.6-next.0 ### Patch Changes diff --git a/packages/backend-openapi-utils/README.md b/packages/backend-openapi-utils/README.md index 33f2200f81..bb420f49fa 100644 --- a/packages/backend-openapi-utils/README.md +++ b/packages/backend-openapi-utils/README.md @@ -4,6 +4,8 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Based on the [`oatx`](https://github.com/varanauskas/oatx) library and adapted to override Express values. +Only supports OpenAPI 3.1 specifications. + ## Getting Started ### Configuration diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 0593224dc2..c8a7befc6c 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-openapi-utils", - "version": "0.6.6-next.0", + "version": "0.6.7-next.1", "description": "OpenAPI typescript support.", "backstage": { "role": "node-library" diff --git a/packages/backend-openapi-utils/src/stub.ts b/packages/backend-openapi-utils/src/stub.ts index 717e2ae19b..b595f357bc 100644 --- a/packages/backend-openapi-utils/src/stub.ts +++ b/packages/backend-openapi-utils/src/stub.ts @@ -54,6 +54,7 @@ export function getOpenApiSpecRoute(baseUrl: string) { /** * Create a router with validation middleware. This is used by typing methods to create an * "OpenAPI router" with all of the expected validation + metadata. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. @@ -115,6 +116,7 @@ function createRouterWithValidation( /** * Create a new OpenAPI router with some default middleware. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. @@ -132,6 +134,7 @@ export function createValidatedOpenApiRouter( /** * Create a new OpenAPI router with some default middleware. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index af82bacd27..5dd9cd0e42 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/backend-plugin-api +## 1.8.0-next.1 + +### Minor Changes + +- 015668c: Added `cancelTask` method to the `SchedulerService` interface and implementation, allowing cancellation of currently running scheduled tasks. For global tasks, the database lock is released and a periodic liveness check aborts the running task function. For local tasks, the task's abort signal is triggered directly. A new `POST /.backstage/scheduler/v1/tasks/:id/cancel` endpoint is also available. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 1.7.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 1.7.0 + +### Minor Changes + +- f1d29b4: Added support for extension point factories. This makes it possible to call `registerExtensionPoint` with a single options argument and provide a factory for the extension point rather than a direct implementation. The factory is passed a context with a `reportModuleStartupFailure` method that makes it possible for plugins to report and attribute startup errors to the module that consumed the extension point. +- bb9b471: Plugin IDs that do not match the standard format are deprecated (letters, digits, and dashes only, starting with a letter). Plugin IDs that do no match this format will be rejected in a future release. + + In addition, plugin IDs that don't match the legacy pattern that also allows underscores, with be rejected. + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/cli-common@0.1.18 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + ## 1.7.0-next.1 ### Minor Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 6316523f28..8f674cda44 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "1.7.0-next.1", + "version": "1.8.0-next.1", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index e06e0c45d2..0f2c220609 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { AnyZodObject } from 'zod'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BasicPermission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; @@ -31,6 +32,7 @@ export type ActionsRegistryActionOptions< input: (zod: typeof z) => TInputSchema; output: (zod: typeof z) => TOutputSchema; }; + visibilityPermission?: BasicPermission; attributes?: { destructive?: boolean; idempotent?: boolean; @@ -82,6 +84,7 @@ export interface ActionsService { // @alpha (undocumented) export type ActionsServiceAction = { id: string; + pluginId: string; name: string; title: string; description: string; @@ -103,6 +106,150 @@ export const actionsServiceRef: ServiceRef< 'singleton' >; +// @alpha +export interface MetricAdvice { + explicitBucketBoundaries?: number[]; +} + +// @alpha +export interface MetricAttributes { + // (undocumented) + [attributeKey: string]: MetricAttributeValue | undefined; +} + +// @alpha +export type MetricAttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; + +// @alpha +export interface MetricOptions { + advice?: MetricAdvice; + description?: string; + unit?: string; +} + +// @alpha +export interface MetricsService { + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter; + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge; + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram; + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter; + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge; + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter; + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter; +} + +// @alpha +export interface MetricsServiceCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + add(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + record(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceHistogram< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + record(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceObservable< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + addCallback(callback: MetricsServiceObservableCallback): void; + // (undocumented) + removeCallback(callback: MetricsServiceObservableCallback): void; +} + +// @alpha +export type MetricsServiceObservableCallback< + TAttributes extends MetricAttributes = MetricAttributes, +> = ( + observableResult: MetricsServiceObservableResult, +) => void | Promise; + +// @alpha +export type MetricsServiceObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export type MetricsServiceObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export interface MetricsServiceObservableResult< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + observe(value: number, attributes?: TAttributes): void; +} + +// @alpha +export type MetricsServiceObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export const metricsServiceRef: ServiceRef< + MetricsService, + 'plugin', + 'singleton' +>; + +// @alpha +export interface MetricsServiceUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + add(value: number, attributes?: TAttributes): void; +} + // @public (undocumented) export interface RootSystemMetadataService { // (undocumented) diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 504be97ec6..96d1a02c0a 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -643,6 +643,7 @@ export interface RootServiceFactoryOptions< // @public export interface SchedulerService { + cancelTask(id: string): Promise; createScheduledTaskRunner( schedule: SchedulerServiceTaskScheduleDefinition, ): SchedulerServiceTaskRunner; diff --git a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts index 9a7820e354..a707e350e9 100644 --- a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { z, AnyZodObject } from 'zod'; +import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService, BackstageCredentials, @@ -42,6 +43,7 @@ export type ActionsRegistryActionOptions< input: (zod: typeof z) => TInputSchema; output: (zod: typeof z) => TOutputSchema; }; + visibilityPermission?: BasicPermission; attributes?: { destructive?: boolean; idempotent?: boolean; diff --git a/packages/backend-plugin-api/src/alpha/ActionsService.ts b/packages/backend-plugin-api/src/alpha/ActionsService.ts index 528b6cea67..6e432e962f 100644 --- a/packages/backend-plugin-api/src/alpha/ActionsService.ts +++ b/packages/backend-plugin-api/src/alpha/ActionsService.ts @@ -22,6 +22,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; */ export type ActionsServiceAction = { id: string; + pluginId: string; name: string; title: string; description: string; diff --git a/packages/backend-plugin-api/src/alpha/MetricsService.ts b/packages/backend-plugin-api/src/alpha/MetricsService.ts new file mode 100644 index 0000000000..1c92ea9e09 --- /dev/null +++ b/packages/backend-plugin-api/src/alpha/MetricsService.ts @@ -0,0 +1,273 @@ +/* + * Copyright 2026 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. + */ + +/** + * Attribute values that can be attached to metric measurements. + * + * @alpha + */ +export type MetricAttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; + +/** + * A set of key-value pairs that can be attached to metric measurements. + * + * @alpha + */ +export interface MetricAttributes { + [attributeKey: string]: MetricAttributeValue | undefined; +} + +/** + * Advisory options that influence aggregation configuration. + * + * @alpha + */ +export interface MetricAdvice { + /** + * Hint the explicit bucket boundaries for histogram aggregation. + */ + explicitBucketBoundaries?: number[]; +} + +/** + * Options for creating a metric instrument. + * + * @alpha + */ +export interface MetricOptions { + /** + * The description of the Metric. + */ + description?: string; + /** + * The unit of the Metric values. + */ + unit?: string; + /** + * Advisory options that influence aggregation configuration. + */ + advice?: MetricAdvice; +} + +/** + * A counter metric that only supports non-negative increments. + * + * @alpha + */ +export interface MetricsServiceCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + add(value: number, attributes?: TAttributes): void; +} + +/** + * A counter metric that supports both positive and negative increments. + * + * @alpha + */ +export interface MetricsServiceUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + add(value: number, attributes?: TAttributes): void; +} + +/** + * A histogram metric for recording distributions of values. + * + * @alpha + */ +export interface MetricsServiceHistogram< + TAttributes extends MetricAttributes = MetricAttributes, +> { + record(value: number, attributes?: TAttributes): void; +} + +/** + * A gauge metric for recording instantaneous values. + * + * @alpha + */ +export interface MetricsServiceGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> { + record(value: number, attributes?: TAttributes): void; +} + +/** + * The result object passed to observable metric callbacks. + * + * @alpha + */ +export interface MetricsServiceObservableResult< + TAttributes extends MetricAttributes = MetricAttributes, +> { + observe(value: number, attributes?: TAttributes): void; +} + +/** + * A callback function for observable metrics. Called whenever a metric + * collection is initiated. + * + * @alpha + */ +export type MetricsServiceObservableCallback< + TAttributes extends MetricAttributes = MetricAttributes, +> = ( + observableResult: MetricsServiceObservableResult, +) => void | Promise; + +/** + * An observable metric instrument that reports values via callbacks. + * + * @alpha + */ +export interface MetricsServiceObservable< + TAttributes extends MetricAttributes = MetricAttributes, +> { + addCallback(callback: MetricsServiceObservableCallback): void; + removeCallback(callback: MetricsServiceObservableCallback): void; +} + +/** + * An observable counter metric that reports non-negative sums via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * An observable counter metric that reports sums that can go up or down + * via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * An observable gauge metric that reports instantaneous values via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * A service that provides a facility for emitting metrics. + * + * @alpha + */ +export interface MetricsService { + /** + * Creates a new counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The counter metric. + */ + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter; + + /** + * Creates a new up-down counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The up-down counter metric. + */ + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter; + + /** + * Creates a new histogram metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The histogram metric. + */ + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram; + + /** + * Creates a new gauge metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The gauge metric. + */ + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge; + + /** + * Creates a new observable counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable counter metric. + */ + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter; + + /** + * Creates a new observable up-down counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable up-down counter metric. + */ + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter; + + /** + * Creates a new observable gauge metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable gauge metric. + */ + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge; +} diff --git a/packages/backend-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts index 56f02f5275..c487686338 100644 --- a/packages/backend-plugin-api/src/alpha/index.ts +++ b/packages/backend-plugin-api/src/alpha/index.ts @@ -27,8 +27,27 @@ export type { export type { ActionsService, ActionsServiceAction } from './ActionsService'; +export type { + MetricsService, + MetricAdvice, + MetricAttributes, + MetricAttributeValue, + MetricOptions, + MetricsServiceCounter, + MetricsServiceUpDownCounter, + MetricsServiceHistogram, + MetricsServiceGauge, + MetricsServiceObservable, + MetricsServiceObservableCallback, + MetricsServiceObservableCounter, + MetricsServiceObservableGauge, + MetricsServiceObservableResult, + MetricsServiceObservableUpDownCounter, +} from './MetricsService'; + export { actionsRegistryServiceRef, actionsServiceRef, + metricsServiceRef, rootSystemMetadataServiceRef, } from './refs'; diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts index a890271364..bd87ea8467 100644 --- a/packages/backend-plugin-api/src/alpha/refs.ts +++ b/packages/backend-plugin-api/src/alpha/refs.ts @@ -56,3 +56,14 @@ export const rootSystemMetadataServiceRef = createServiceRef< id: 'alpha.core.rootSystemMetadata', scope: 'root', }); + +/** + * Service for managing metrics. + * + * @alpha + */ +export const metricsServiceRef = createServiceRef< + import('./MetricsService').MetricsService +>({ + id: 'alpha.core.metrics', +}); diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts index 6c0936f85c..a17c82c6f2 100644 --- a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { z, AnyZodObject } from 'zod'; +import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService } from './LoggerService'; import { BackstageCredentials } from './AuthService'; @@ -40,6 +41,7 @@ export type ActionsRegistryActionOptions< input: (zod: typeof z) => TInputSchema; output: (zod: typeof z) => TOutputSchema; }; + visibilityPermission?: BasicPermission; attributes?: { destructive?: boolean; idempotent?: boolean; diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index b1a0e17df8..2748b8fac7 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -304,6 +304,16 @@ export interface SchedulerService { */ triggerTask(id: string): Promise; + /** + * Cancels a currently running task by ID, marking it as idle. + * + * If the task doesn't exist, a NotFoundError is thrown. If the task is + * not currently running, a ConflictError is thrown. + * + * @param id - The task ID + */ + cancelTask(id: string): Promise; + /** * Schedules a task function for recurring runs. * diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 360da7b068..4ffe1fe7f3 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/backend-test-utils +## 1.11.1-next.2 + +### Patch Changes + +- 164711a: Added `cancelTask` to `MockSchedulerService` and mock scheduler service factory. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/backend-app-api@1.6.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 1.11.1-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 1.11.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds a new metrics service mock to be leveraged in tests +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 1.11.0 + +### Minor Changes + +- 42abfb1: Added `createServiceMock`, a public utility for creating `ServiceMock` instances for custom service refs. This allows plugin authors to define mock creators for their own services following the same pattern as the built-in `mockServices` mocks. + +### Patch Changes + +- f1d29b4: Updated `startTestBackend` to support factory-based extension points (v1.1 format) in addition to the existing direct implementation format. +- 7455dae: Use node prefix on native imports +- 68eb322: Added `@types/jest` as an optional peer dependency, since jest types are exposed in the public API surface. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-app-api@1.5.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-events-node@0.4.19 + ## 1.11.0-next.1 ### Minor Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index f762c2ae38..c2d2268878 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "1.11.0-next.1", + "version": "1.11.1-next.2", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md index ca6f47191d..b6bf9e883d 100644 --- a/packages/backend-test-utils/report-alpha.api.md +++ b/packages/backend-test-utils/report-alpha.api.md @@ -12,6 +12,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @alpha (undocumented) @@ -43,6 +44,16 @@ export namespace actionsServiceMock { ) => ServiceMock; } +// @alpha (undocumented) +export namespace metricsServiceMock { + const // (undocumented) + factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; +} + // @alpha export class MockActionsRegistry implements ActionsRegistryService, ActionsService diff --git a/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts b/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts new file mode 100644 index 0000000000..301d47fb2e --- /dev/null +++ b/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts @@ -0,0 +1,59 @@ +/* + * 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 { createServiceMock } from './alphaCreateServiceMock'; +import { + MetricsService, + metricsServiceRef, +} from '@backstage/backend-plugin-api/alpha'; +import { metricsServiceFactory } from '@backstage/backend-defaults/alpha'; + +/** + * @alpha + */ +export namespace metricsServiceMock { + export const factory = () => metricsServiceFactory; + + export const mock = createServiceMock( + metricsServiceRef, + () => ({ + createCounter: jest.fn().mockImplementation(() => ({ + add: jest.fn(), + })), + createUpDownCounter: jest.fn().mockImplementation(() => ({ + add: jest.fn(), + })), + createHistogram: jest.fn().mockImplementation(() => ({ + record: jest.fn(), + })), + createGauge: jest.fn().mockImplementation(() => ({ + record: jest.fn(), + })), + createObservableCounter: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + createObservableUpDownCounter: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + createObservableGauge: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + }), + ); +} diff --git a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts index 94e5987afd..94d844746f 100644 --- a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts +++ b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts @@ -17,7 +17,7 @@ import { BackstageCredentials, LoggerService, } from '@backstage/backend-plugin-api'; -import { ForwardedError, InputError, NotFoundError } from '@backstage/errors'; +import { InputError, NotFoundError } from '@backstage/errors'; import { JsonObject, JsonValue } from '@backstage/types'; import { z, AnyZodObject } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -82,6 +82,7 @@ export class MockActionsRegistry return { actions: Array.from(this.actions.entries()).map(([id, action]) => ({ id, + pluginId: 'test', name: action.name, title: action.title, description: action.description, @@ -126,31 +127,24 @@ export class MockActionsRegistry throw new InputError(`Invalid input to action "${opts.id}"`, input.error); } - try { - const result = await action.action({ - input: input.data, - credentials: opts.credentials ?? mockCredentials.none(), - logger: this.logger, - }); + const result = await action.action({ + input: input.data, + credentials: opts.credentials ?? mockCredentials.none(), + logger: this.logger, + }); - const output = action.schema?.output - ? action.schema.output(z).safeParse(result?.output) - : ({ success: true, data: result?.output } as const); + const output = action.schema?.output + ? action.schema.output(z).safeParse(result?.output) + : ({ success: true, data: result?.output } as const); - if (!output.success) { - throw new InputError( - `Invalid output from action "${opts.id}"`, - output.error, - ); - } - - return { output: output.data }; - } catch (error) { - throw new ForwardedError( - `Failed execution of action "${opts.id}"`, - error, + if (!output.success) { + throw new InputError( + `Invalid output from action "${opts.id}"`, + output.error, ); } + + return { output: output.data }; } register< diff --git a/packages/backend-test-utils/src/alpha/services/index.ts b/packages/backend-test-utils/src/alpha/services/index.ts index ac92033ff8..527bb1822f 100644 --- a/packages/backend-test-utils/src/alpha/services/index.ts +++ b/packages/backend-test-utils/src/alpha/services/index.ts @@ -17,4 +17,5 @@ export { actionsRegistryServiceMock } from './ActionsRegistryServiceMock'; export { MockActionsRegistry } from './MockActionsRegistry'; export { actionsServiceMock } from './ActionsServiceMock'; +export { metricsServiceMock } from './MetricsServiceMock'; export { type ServiceMock } from './alphaCreateServiceMock'; diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.test.ts b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts index faf4e3e26d..2668fa7205 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.test.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts @@ -206,6 +206,68 @@ describe('MockSchedulerService', () => { await expect(isDone()).resolves.toBe(true); }); + it('should cancel a running task and allow re-triggering with a fresh signal', async () => { + const scheduler = new MockSchedulerService(); + const signals: AbortSignal[] = []; + + scheduler.scheduleTask({ + ...baseOpts, + id: 'test', + fn: async signal => { + signals.push(signal); + // Simulate long-running work that respects cancellation + await new Promise((resolve, reject) => { + if (signal.aborted) { + reject(new Error('aborted')); + return; + } + signal.addEventListener('abort', () => reject(new Error('aborted'))); + setTimeout(1).then(resolve); + }); + }, + }); + + // First run completes normally + await scheduler.triggerTask('test'); + expect(signals).toHaveLength(1); + expect(signals[0].aborted).toBe(false); + + // Start a task that will block until cancelled + const blockingScheduler = new MockSchedulerService(); + let resolveBlock: (() => void) | undefined; + blockingScheduler.scheduleTask({ + ...baseOpts, + id: 'blocking', + fn: async signal => { + signals.push(signal); + await new Promise((resolve, reject) => { + signal.addEventListener('abort', () => reject(new Error('aborted'))); + resolveBlock = resolve; + }); + }, + }); + + const triggerPromise = blockingScheduler.triggerTask('blocking'); + // Give the task fn time to start + await setTimeout(1); + + await blockingScheduler.cancelTask('blocking'); + await triggerPromise.catch(() => {}); + + expect(signals).toHaveLength(2); + expect(signals[1].aborted).toBe(true); + + // Re-trigger should get a fresh non-aborted signal + resolveBlock = undefined; + const triggerPromise2 = blockingScheduler.triggerTask('blocking'); + await setTimeout(1); + resolveBlock!(); + await triggerPromise2; + + expect(signals).toHaveLength(3); + expect(signals[2].aborted).toBe(false); + }); + it('should abort tasks when shutting down', async () => { let taskSignal: AbortSignal | undefined; diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts index aafd6d98ec..3309ebc954 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -23,6 +23,7 @@ import { SchedulerServiceTaskRunner, SchedulerServiceTaskScheduleDefinition, } from '@backstage/backend-plugin-api'; +import { ConflictError, NotFoundError } from '@backstage/errors'; import { createDeferred, DeferredPromise } from '@backstage/types'; export class MockSchedulerService implements SchedulerService { @@ -95,10 +96,22 @@ export class MockSchedulerService implements SchedulerService { }); } + async cancelTask(id: string): Promise { + const task = this.#tasks.get(id); + if (!task) { + throw new NotFoundError(`Task ${id} not found`); + } + if (!this.#runningTasks.has(id)) { + throw new ConflictError(`Task ${id} is not running`); + } + task.abortControllers.abort(); + task.abortControllers = new AbortController(); + } + async triggerTask(id: string): Promise { const task = this.#tasks.get(id); if (!task) { - throw new Error(`Task ${id} not found`); + throw new NotFoundError(`Task ${id} not found`); } if (this.#runningTasks.has(id)) { return; diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 84783f04d7..4d54269b98 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -526,6 +526,7 @@ export namespace mockServices { getScheduledTasks: jest.fn(), scheduleTask: jest.fn(), triggerTask: jest.fn(), + cancelTask: jest.fn(), })); } diff --git a/packages/backend-test-utils/src/wiring/TestBackend.ts b/packages/backend-test-utils/src/wiring/TestBackend.ts index 43a63a49e5..11decb883d 100644 --- a/packages/backend-test-utils/src/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/wiring/TestBackend.ts @@ -43,6 +43,7 @@ import { HostDiscovery } from '@backstage/backend-defaults/discovery'; import { actionsRegistryServiceMock, actionsServiceMock, + metricsServiceMock, } from '../alpha/services'; /** @public */ @@ -92,6 +93,7 @@ export const defaultServiceFactories = [ // Alpha services actionsRegistryServiceMock.factory(), actionsServiceMock.factory(), + metricsServiceMock.factory(), ]; /** diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 9ddbc73c20..18d3271cc1 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,170 @@ # example-backend +## 0.0.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-scaffolder-backend@3.2.0-next.2 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.2 + - @backstage/plugin-app-backend@0.5.12-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.1 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.1 + - @backstage/plugin-devtools-backend@0.5.15-next.1 + - @backstage/plugin-events-backend@0.6.0-next.2 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.2 + - @backstage/plugin-notifications-backend@0.6.3-next.1 + - @backstage/plugin-permission-backend@0.7.10-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-proxy-backend@0.6.11-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.2 + - @backstage/plugin-search-backend@2.1.0-next.2 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + - @backstage/plugin-signals-backend@0.3.13-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.2 + +## 0.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@3.2.0-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-search-backend@2.1.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + +## 0.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-scaffolder-backend@3.1.4-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-kubernetes-backend@0.21.2-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + - @backstage/plugin-search-backend@2.0.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + - @backstage/plugin-techdocs-backend@2.1.6-next.0 + +## 0.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.11 + - @backstage/plugin-catalog-backend-module-openapi@0.2.19 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.17 + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.6 + - @backstage/plugin-search-backend-module-techdocs@0.4.11 + - @backstage/plugin-search-backend-module-catalog@0.3.12 + - @backstage/plugin-search-backend-module-explore@0.3.11 + - @backstage/plugin-notifications-backend@0.6.2 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-kubernetes-backend@0.21.1 + - @backstage/plugin-permission-backend@0.7.9 + - @backstage/plugin-scaffolder-backend@3.1.3 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-devtools-backend@0.5.14 + - @backstage/plugin-techdocs-backend@2.1.5 + - @backstage/plugin-signals-backend@0.3.12 + - @backstage/plugin-events-backend@0.5.11 + - @backstage/plugin-search-backend@2.0.12 + - @backstage/plugin-proxy-backend@0.6.10 + - @backstage/plugin-app-backend@0.5.11 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-mcp-actions-backend@0.1.9 + - @backstage/plugin-auth-backend-module-github-provider@0.5.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.16 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.4 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.8 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.16 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.19 + ## 0.0.47-next.2 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index e172623734..bdd5610ef5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.47-next.2", + "version": "0.0.48-next.2", "backstage": { "role": "backend" }, @@ -66,7 +66,7 @@ "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-signals-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", - "@opentelemetry/auto-instrumentations-node": "^0.67.0", + "@opentelemetry/auto-instrumentations-node": "^0.71.0", "@opentelemetry/exporter-prometheus": "^0.211.0", "@opentelemetry/sdk-node": "^0.211.0", "example-app": "link:../app" diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index b78d214482..f33963560a 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/catalog-client +## 1.14.0-next.2 + +### Minor Changes + +- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error. + +## 1.14.0-next.1 + +### Minor Changes + +- 972f686: Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for the `query` field in `getEntityFacets` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## 1.13.1-next.0 + +### Patch Changes + +- d2494d6: Minor update to catalog client docs +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## 1.13.0 + +### Minor Changes + +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries + +### Patch Changes + +- 9cf6762: Improved the `InMemoryCatalogClient` test utility to support ordering, pagination, full-text search, and field projection for entity query methods. Also fixed `getEntityFacets` to correctly handle multi-valued fields. +- Updated dependencies + - @backstage/filter-predicates@0.1.0 + ## 1.12.2-next.0 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 3ebf43762d..bc4ad3c388 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.12.2-next.0", + "version": "1.14.0-next.2", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-client/report.api.md b/packages/catalog-client/report.api.md index 58fc4bc8a1..73eb280d87 100644 --- a/packages/catalog-client/report.api.md +++ b/packages/catalog-client/report.api.md @@ -7,14 +7,15 @@ import type { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common'; import type { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { FilterPredicate } from '@backstage/filter-predicates'; -import { SerializedError } from '@backstage/errors'; +import type { FilterPredicate } from '@backstage/filter-predicates'; +import type { SerializedError } from '@backstage/errors'; // @public export type AddLocationRequest = { type?: string; target: string; dryRun?: boolean; + onConflict?: 'refresh' | 'reject'; }; // @public @@ -236,6 +237,7 @@ export interface GetEntitiesByRefsRequest { entityRefs: string[]; fields?: EntityFieldsQuery | undefined; filter?: EntityFilterQuery; + query?: FilterPredicate; } // @public @@ -280,6 +282,7 @@ export interface GetEntityAncestorsResponse { export interface GetEntityFacetsRequest { facets: string[]; filter?: EntityFilterQuery; + query?: FilterPredicate; } // @public @@ -320,6 +323,7 @@ export type QueryEntitiesInitialRequest = { limit?: number; offset?: number; filter?: EntityFilterQuery; + query?: FilterPredicate; orderFields?: EntityOrderQuery; fullTextFilter?: { term: string; diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index d62323852e..70265bd6cb 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -302,6 +302,103 @@ describe('CatalogClient', () => { expect(response).toEqual({ items: [entity, undefined] }); }); + + it('sends only query predicate in the body when query is provided without filter', async () => { + expect.assertions(3); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe(''); + await expect(req.json()).resolves.toEqual({ + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + }); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); + + it('merges filter and query into $all predicate when both are provided', async () => { + expect.assertions(4); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe(''); + const body = await req.json(); + expect(body.entityRefs).toEqual(['k:n/a']); + expect(body.query).toEqual({ + $all: [{ kind: 'Component' }, { kind: 'API' }], + }); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + filter: { kind: ['API'] }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); + + it('sends filter as query parameter when only filter is provided (backward compat)', async () => { + expect.assertions(4); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe('?filter=kind%3DAPI'); + const body = await req.json(); + expect(body).toEqual({ entityRefs: ['k:n/a'] }); + expect(body.query).toBeUndefined(); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + filter: { kind: ['API'] }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); }); describe('queryEntities', () => { @@ -540,6 +637,350 @@ describe('CatalogClient', () => { }); }); + describe('queryEntities with predicate-based queries (POST endpoint)', () => { + const defaultResponse = { + items: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'service-1', + namespace: 'default', + }, + spec: { + type: 'service', + owner: 'team-a', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'service-2', + namespace: 'default', + }, + spec: { + type: 'service', + owner: 'team-b', + }, + }, + ], + totalItems: 2, + pageInfo: {}, + }; + + it('should use POST endpoint when query is provided', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.method).toBe('POST'); + expect(req.body).toMatchObject({ + query: { kind: 'component' }, + limit: 20, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities({ + query: { kind: 'component' }, + limit: 20, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + expect(response.items).toEqual(defaultResponse.items); + expect(response.totalItems).toBe(2); + }); + + it('should support $all operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $any operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $not operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $not: { 'spec.lifecycle': 'experimental' }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $not: { 'spec.lifecycle': 'experimental' }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $exists operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + 'spec.owner': { $exists: true }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + 'spec.owner': { $exists: true }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $in operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + 'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + 'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support complex nested predicates', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $all: [ + { kind: 'component' }, + { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + { + $not: { + 'spec.lifecycle': 'experimental', + }, + }, + ], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $all: [ + { kind: 'component' }, + { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + { + $not: { + 'spec.lifecycle': 'experimental', + }, + }, + ], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send orderFields with correct format', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + ]); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + orderFields: { field: 'metadata.name', order: 'asc' }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send multiple orderFields with correct format', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + { field: 'spec.type', order: 'desc' }, + ]); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + orderFields: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'spec.type', order: 'desc' }, + ], + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send limit and offset parameters in the body', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.limit).toBe(50); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + limit: 50, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should paginate using POST when cursor contains a query', async () => { + // Simulate a cursor that contains a query predicate (as the server would encode it) + const cursorPayload = Buffer.from( + JSON.stringify({ + orderFields: [], + orderFieldValues: [], + isPrevious: false, + query: { kind: 'component' }, + totalItems: 100, + }), + ).toString('base64'); + + const page2Response = { + items: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { name: 'service-3', namespace: 'default' }, + }, + ], + totalItems: 100, + pageInfo: {}, + }; + + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.method).toBe('POST'); + expect(req.body).toMatchObject({ cursor: cursorPayload }); + return res(ctx.json(page2Response)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities({ + cursor: cursorPayload, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + expect(response.items).toEqual(page2Response.items); + expect(response.totalItems).toBe(100); + }); + + it('should use GET endpoint for cursor without query', async () => { + // A cursor that does NOT contain a query field should go to GET + const cursorPayload = Buffer.from( + JSON.stringify({ + orderFields: [], + orderFieldValues: [], + isPrevious: false, + totalItems: 50, + }), + ).toString('base64'); + + const mockedGetEndpoint = jest.fn().mockImplementation((_req, res, ctx) => + res( + ctx.json({ + items: [], + totalItems: 50, + pageInfo: {}, + }), + ), + ); + + const mockedPostEndpoint = jest.fn(); + + server.use( + rest.get(`${mockBaseUrl}/entities/by-query`, mockedGetEndpoint), + rest.post(`${mockBaseUrl}/entities/by-query`, mockedPostEndpoint), + ); + + await client.queryEntities({ cursor: cursorPayload }); + + expect(mockedGetEndpoint).toHaveBeenCalledTimes(1); + expect(mockedPostEndpoint).not.toHaveBeenCalled(); + }); + + it('should handle errors from POST endpoint', async () => { + const mockedEndpoint = jest + .fn() + .mockImplementation((_req, res, ctx) => res(ctx.status(400))); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await expect(() => + client.queryEntities({ query: { kind: 'component' } }), + ).rejects.toThrow(/Request failed with 400/); + }); + }); + describe('streamEntities', () => { const defaultResponse: QueryEntitiesResponse = { items: [ diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index a537e8b3a5..cf363e1823 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -20,7 +20,8 @@ import { parseEntityRef, stringifyLocationRef, } from '@backstage/catalog-model'; -import { ResponseError } from '@backstage/errors'; +import { InputError, ResponseError } from '@backstage/errors'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { AddLocationRequest, AddLocationResponse, @@ -46,10 +47,17 @@ import { StreamEntitiesRequest, ValidateEntityResponse, } from './types/api'; -import { isQueryEntitiesInitialRequest, splitRefsIntoChunks } from './utils'; +import { + convertFilterToPredicate, + isQueryEntitiesInitialRequest, + splitRefsIntoChunks, + cursorContainsQuery, +} from './utils'; import { DefaultApiClient, + GetEntitiesByQuery, GetLocationsByQueryRequest, + QueryEntitiesByPredicateRequest, TypedResponse, } from './schema/openapi'; import type { @@ -229,11 +237,36 @@ export class CatalogClient implements CatalogApi { request: GetEntitiesByRefsRequest, options?: CatalogRequestOptions, ): Promise { + const { filter, query } = request; + + // Only convert and merge if both filter and query are provided, or if + // query alone is provided. When only filter is given, preserve the old + // query-parameter behavior for backward compatibility. + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if (typeof query !== 'object' || query === null || Array.isArray(query)) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = { $all: [filterPredicate, converted] }; + } + } + const getOneChunk = async (refs: string[]) => { const response = await this.apiClient.getEntitiesByRefs( { - body: { entityRefs: refs, fields: request.fields }, - query: { filter: this.getFilterValue(request.filter) }, + body: { + entityRefs: refs, + fields: request.fields, + ...(filterPredicate && { + query: filterPredicate as unknown as { [key: string]: any }, + }), + }, + query: filterPredicate + ? {} + : { filter: this.getFilterValue(request.filter) }, }, options, ); @@ -266,11 +299,26 @@ export class CatalogClient implements CatalogApi { request: QueryEntitiesRequest = {}, options?: CatalogRequestOptions, ): Promise { - const params: Partial< - Parameters[0]['query'] - > = {}; + const isInitialRequest = isQueryEntitiesInitialRequest(request); - if (isQueryEntitiesInitialRequest(request)) { + // Route to POST endpoint if query predicate is provided (initial request) + if (isInitialRequest && request.query) { + return this.queryEntitiesByPredicate(request, options); + } + + // Route to POST endpoint if cursor contains a query predicate (pagination) + // TODO(freben): It's costly and non-opaque to have to introspect the cursor + // like this. It should be refactored in the future to not need this. + // Suggestion: make the GET and POST endpoints understand the same cursor + // format, and pick which one to call ONLY based on whether the cursor size + // risks hitting url length limits + if (!isInitialRequest && cursorContainsQuery(request.cursor)) { + return this.queryEntitiesByPredicate(request, options); + } + + const params: Partial = {}; + + if (isInitialRequest) { const { fields = [], filter, @@ -320,6 +368,84 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Query entities using predicate-based filters (POST endpoint). + * @internal + */ + private async queryEntitiesByPredicate( + request: QueryEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise { + const body: QueryEntitiesByPredicateRequest = {}; + + if (isQueryEntitiesInitialRequest(request)) { + const { + filter, + query, + limit, + offset, + orderFields, + fullTextFilter, + fields, + } = request; + + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if ( + typeof query !== 'object' || + query === null || + Array.isArray(query) + ) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + } + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = filterPredicate + ? { $all: [filterPredicate, converted] } + : converted; + } + if (filterPredicate !== undefined) { + body.query = filterPredicate as unknown as { [key: string]: any }; + } + + if (limit !== undefined) { + body.limit = limit; + } + if (offset !== undefined) { + body.offset = offset; + } + if (orderFields !== undefined) { + body.orderBy = [orderFields].flat(); + } + if (fullTextFilter) { + body.fullTextFilter = fullTextFilter; + } + if (fields?.length) { + body.fields = fields; + } + } else { + body.cursor = request.cursor; + if (request.limit !== undefined) { + body.limit = request.limit; + } + if (request.fields?.length) { + body.fields = request.fields; + } + } + + const res = await this.requestRequired( + await this.apiClient.queryEntitiesByPredicate({ body }, options), + ); + + return { + items: res.items, + totalItems: res.totalItems, + pageInfo: res.pageInfo, + }; + } + /** * {@inheritdoc CatalogApi.getEntityByRef} */ @@ -378,7 +504,13 @@ export class CatalogClient implements CatalogApi { request: GetEntityFacetsRequest, options?: CatalogRequestOptions, ): Promise { - const { filter = [], facets } = request; + const { filter = [], query, facets } = request; + + // Route to POST endpoint if query predicate is provided + if (query) { + return this.getEntityFacetsByPredicate(request, options); + } + return await this.requestOptional( await this.apiClient.getEntityFacets( { @@ -389,6 +521,45 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Get entity facets using predicate-based filters (POST endpoint). + * @internal + */ + private async getEntityFacetsByPredicate( + request: GetEntityFacetsRequest, + options?: CatalogRequestOptions, + ): Promise { + const { filter, query, facets } = request; + + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if (typeof query !== 'object' || query === null || Array.isArray(query)) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + } + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = filterPredicate + ? { $all: [filterPredicate, converted] } + : converted; + } + + return await this.requestOptional( + await this.apiClient.queryEntityFacetsByPredicate( + { + body: { + facets, + ...(filterPredicate && { + query: filterPredicate as unknown as { [key: string]: any }, + }), + }, + }, + options, + ), + ); + } + /** * {@inheritdoc CatalogApi.addLocation} */ @@ -396,12 +567,15 @@ export class CatalogClient implements CatalogApi { request: AddLocationRequest, options?: CatalogRequestOptions, ): Promise { - const { type = 'url', target, dryRun } = request; + const { type = 'url', target, dryRun, onConflict } = request; const response = await this.apiClient.createLocation( { body: { type, target }, - query: { dryRun: dryRun ? 'true' : undefined }, + query: { + dryRun: dryRun ? 'true' : undefined, + onConflict, + }, }, options, ); @@ -535,9 +709,7 @@ export class CatalogClient implements CatalogApi { } while (cursor); } - // - // Private methods - // + // #region Private methods private async requestIgnored(response: Response): Promise { if (!response.ok) { @@ -588,4 +760,6 @@ export class CatalogClient implements CatalogApi { } return filters; } + + // #endregion } diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts index 4530cd795c..4b3e088fe2 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts @@ -29,6 +29,8 @@ import { Entity } from '../models/Entity.model'; import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model'; import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model'; import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model'; +import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model'; +import { QueryEntityFacetsByPredicateRequest } from '../models/QueryEntityFacetsByPredicateRequest.model'; import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model'; import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model'; import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model'; @@ -139,6 +141,18 @@ export type GetEntityFacets = { filter?: Array; }; }; +/** + * @public + */ +export type QueryEntitiesByPredicate = { + body: QueryEntitiesByPredicateRequest; +}; +/** + * @public + */ +export type QueryEntityFacetsByPredicate = { + body: QueryEntityFacetsByPredicateRequest; +}; /** * @public */ @@ -164,6 +178,7 @@ export type CreateLocation = { body: CreateLocationRequest; query: { dryRun?: string; + onConflict?: 'refresh' | 'reject'; }; }; /** @@ -246,9 +261,9 @@ export class DefaultApiClient { /** * Get all entities matching a given filter. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it\'ll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` * @param limit - Number of records to return in the response. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param offset - Number of records to skip in the query page. * @param after - Pointer to the previous page of results. * @param order - @@ -277,12 +292,12 @@ export class DefaultApiClient { /** * Search for entities by a given query. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it\'ll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` * @param limit - Number of records to return in the response. * @param offset - Number of records to skip in the query page. * @param orderField - By default the entities are returned ordered by their internal uid. You can customize the `orderField` query parameters to affect that ordering. For example, to return entities by their name: `/entities/by-query?orderField=metadata.name,asc` Each parameter can be followed by `asc` for ascending lexicographical order or `desc` for descending (reverse) lexicographical order. - * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn\'t possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param fullTextFilterTerm - Text search term. * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. */ @@ -310,7 +325,7 @@ export class DefaultApiClient { /** * Get a batch set of entities given an array of entityRefs. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param getEntitiesByRefsRequest - */ public async getEntitiesByRefs( @@ -337,7 +352,7 @@ export class DefaultApiClient { } /** - * Get an entity's ancestry by entity ref. + * Get an entity\'s ancestry by entity ref. * @param kind - * @param namespace - * @param name - @@ -425,7 +440,7 @@ export class DefaultApiClient { /** * Get all entity facets that match the given filters. * @param facet - - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` */ public async getEntityFacets( // @ts-ignore @@ -449,6 +464,56 @@ export class DefaultApiClient { }); } + /** + * Query entities using predicate-based filters. + * @param queryEntitiesByPredicateRequest - + */ + public async queryEntitiesByPredicate( + // @ts-ignore + request: QueryEntitiesByPredicate, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/entities/by-query`; + + const uri = parser.parse(uriTemplate).expand({}); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify(request.body), + }); + } + + /** + * Get entity facets using predicate-based filters. + * @param queryEntityFacetsByPredicateRequest - + */ + public async queryEntityFacetsByPredicate( + // @ts-ignore + request: QueryEntityFacetsByPredicate, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/entity-facets`; + + const uri = parser.parse(uriTemplate).expand({}); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify(request.body), + }); + } + /** * Refresh the entity related to entityRef. * @param refreshEntityRequest - @@ -528,6 +593,7 @@ export class DefaultApiClient { * Create a location for a given target. * @param createLocationRequest - * @param dryRun - + * @param onConflict - Behavior when the location already exists. \'reject\' (default) returns a 409 error, \'refresh\' triggers a refresh of the existing location entity and returns 201. */ public async createLocation( // @ts-ignore @@ -536,7 +602,7 @@ export class DefaultApiClient { ): Promise> { const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); - const uriTemplate = `/locations{?dryRun}`; + const uriTemplate = `/locations{?dryRun,onConflict}`; const uri = parser.parse(uriTemplate).expand({ ...request.query, diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index ea3d097ec5..06998f5422 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { LocationSpec } from '../models/LocationSpec.model'; /** - * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren't already + * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren\'t already * @public */ export interface AnalyzeLocationExistingEntity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index 2999da3ddf..7b2ba72114 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationEntityField } from '../models/AnalyzeLocationEntityField.model'; import { RecursivePartialEntity } from '../models/RecursivePartialEntity.model'; /** - * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It'll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. + * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It\'ll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. * @public */ export interface AnalyzeLocationGenerateEntity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index 5099a5c8aa..a0a5615f25 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { LocationInput } from '../models/LocationInput.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index a5eac33f7c..d4d8a0fdd9 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationExistingEntity } from '../models/AnalyzeLocationExistingEntity.model'; import { AnalyzeLocationGenerateEntity } from '../models/AnalyzeLocationGenerateEntity.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 610e565b3e..e224386de4 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { Location } from '../models/Location.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index e8bf79a501..0434965888 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { NullableEntity } from '../models/NullableEntity.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index d201466db6..5165faec58 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntitiesQueryResponsePageInfo } from '../models/EntitiesQueryResponsePageInfo.model'; import { Entity } from '../models/Entity.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts index 108a7b15b8..00cc91e89b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export interface Entity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index f2c1116f35..9d1e4d9052 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityAncestryResponseItemsInner } from '../models/EntityAncestryResponseItemsInner.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 9c816e5517..0936545ec7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index 5cc67294e5..540650bcaf 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityFacet } from '../models/EntityFacet.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts index c1e1471045..a35ee87b61 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** @@ -26,7 +25,6 @@ import { EntityLink } from '../models/EntityLink.model'; */ export interface EntityMeta { [key: string]: any; - /** * A list of external hyperlinks related to the entity. */ diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 0e7255f9c6..607c992ace 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -24,4 +24,8 @@ export interface GetEntitiesByRefsRequest { entityRefs: Array; fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index b77b78971f..846b58ba97 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts index c231fccc57..05259caf32 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; import { LocationsQueryResponsePageInfo } from '../models/LocationsQueryResponsePageInfo.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts index 207f16f828..9d5c36325e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ErrorError } from '../models/ErrorError.model'; import { ErrorRequest } from '../models/ErrorRequest.model'; import { ErrorResponse } from '../models/ErrorResponse.model'; @@ -27,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts index 5e59af3327..288b6febb6 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export type NullableEntity = { diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts similarity index 54% rename from plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts index 5b6c2ff9c7..7196156f55 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,13 +17,21 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { DryRun200ResponseAllOfDirectoryContentsInner } from '../models/DryRun200ResponseAllOfDirectoryContentsInner.model'; -import { DryRun200ResponseAllOfStepsInner } from '../models/DryRun200ResponseAllOfStepsInner.model'; +import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; /** * @public */ -export interface DryRun200ResponseAllOf { - steps: Array; - directoryContents?: Array; +export interface QueryEntitiesByPredicateRequest { + cursor?: string; + limit?: number; + offset?: number; + orderBy?: Array; + fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter; + fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts similarity index 84% rename from plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts index 0b33852cc9..e7e971e1ea 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,6 +21,7 @@ /** * @public */ -export interface TaskSecretsAllOf { - backstageToken?: string; +export interface QueryEntitiesByPredicateRequestFullTextFilter { + term?: string; + fields?: Array; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts new file mode 100644 index 0000000000..373ca9715f --- /dev/null +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface QueryEntitiesByPredicateRequestOrderByInner { + field: string; + order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum; +} + +/** + * @public + */ +export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum = + | 'asc' + | 'desc'; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts similarity index 78% rename from plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts index e67240ee54..14fd3a8bc0 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,13 +17,14 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { TaskStatus } from '../models/TaskStatus.model'; /** * @public */ -export interface DryRunResultLogInnerBodyAllOf { - message: string; - status?: TaskStatus; - stepId?: string; +export interface QueryEntityFacetsByPredicateRequest { + facets: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index 3f09c69672..3a607dd878 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { RecursivePartialEntityMeta } from '../models/RecursivePartialEntityMeta.model'; import { RecursivePartialEntityRelation } from '../models/RecursivePartialEntityRelation.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index 2db84dd085..7c2d3bed48 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts deleted file mode 100644 index caca69a622..0000000000 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2026 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. - */ - -// ****************************************************************** -// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * -// ****************************************************************** - -import { EntityLink } from '../models/EntityLink.model'; - -/** - * Metadata fields common to all versions/kinds of entity. - * @public - */ -export interface RecursivePartialEntityMetaAllOf { - /** - * A list of external hyperlinks related to the entity. - */ - links?: Array; - /** - * A list of single-valued strings, to for example classify catalog entities in various ways. - */ - tags?: Array; - /** - * Construct a type with a set of properties K of type T - */ - annotations?: { [key: string]: string }; - /** - * Construct a type with a set of properties K of type T - */ - labels?: { [key: string]: string }; - /** - * A short (typically relatively few words, on one line) description of the entity. - */ - description?: string; - /** - * A display name of the entity, to be presented in user interfaces instead of the `name` property above, when available. This field is sometimes useful when the `name` is cumbersome or ends up being perceived as overly technical. The title generally does not have as stringent format requirements on it, so it may contain special characters and be more explanatory. Do keep it very short though, and avoid situations where a title can be confused with the name of another entity, or where two entities share a title. Note that this is only for display purposes, and may be ignored by some parts of the code. Entity references still always make use of the `name` property, not the title. - */ - title?: string; - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - /** - * The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair. This value is part of the technical identifier of the entity, and as such it will appear in URLs, database tables, entity references, and similar. It is subject to restrictions regarding what characters are allowed. If you want to use a different, more human readable string with fewer restrictions on it in user interfaces, see the `title` field below. - */ - name?: string; - /** - * An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value. - */ - etag?: string; - /** - * A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics. - */ - uid?: string; -} diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index aabf825a57..2a41f2df5c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ValidateEntity400ResponseErrorsInner } from '../models/ValidateEntity400ResponseErrorsInner.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 4582bf2046..f8a7ae4386 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -23,7 +23,6 @@ */ export interface ValidateEntity400ResponseErrorsInner { [key: string]: any; - name: string; message: string; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/index.ts b/packages/catalog-client/src/schema/openapi/generated/models/index.ts index abdb58378c..aaafc8e60e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/index.ts @@ -45,9 +45,12 @@ export * from '../models/LocationsQueryResponse.model'; export * from '../models/LocationsQueryResponsePageInfo.model'; export * from '../models/ModelError.model'; export * from '../models/NullableEntity.model'; +export * from '../models/QueryEntitiesByPredicateRequest.model'; +export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; +export * from '../models/QueryEntityFacetsByPredicateRequest.model'; export * from '../models/RecursivePartialEntity.model'; export * from '../models/RecursivePartialEntityMeta.model'; -export * from '../models/RecursivePartialEntityMetaAllOf.model'; export * from '../models/RecursivePartialEntityRelation.model'; export * from '../models/RefreshEntityRequest.model'; export * from '../models/ValidateEntity400Response.model'; diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts index d867ab1d28..98b5d5c034 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts @@ -370,6 +370,25 @@ describe('InMemoryCatalogClient', () => { { kind: 'CustomKind', metadata: { name: 'e1' } }, ]); }); + + it('supports query predicate filter', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntitiesByRefs({ + entityRefs: ['secondcustomkind:default/e2', 'customkind:default/e1'], + query: { kind: 'CustomKind' }, + }); + expect(result.items).toEqual([undefined, entity1]); + }); + + it('supports both filter and query predicate together', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntitiesByRefs({ + entityRefs: ['customkind:default/e1', 'customkind:other/e3'], + filter: { kind: 'CustomKind' }, + query: { 'metadata.namespace': 'other' }, + }); + expect(result.items).toEqual([undefined, entity3]); + }); }); describe('queryEntities', () => { @@ -683,6 +702,83 @@ describe('InMemoryCatalogClient', () => { ]); }); + it('filters by predicate query', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { kind: 'CustomKind' }, + }); + expect(result.items).toEqual([entity1, entity3]); + expect(result.totalItems).toBe(2); + }); + + it('filters by predicate query with $all', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $all: [{ kind: 'CustomKind' }, { 'spec.type': 'service' }], + }, + }); + expect(result.items).toEqual([entity1, entity3]); + }); + + it('filters by predicate query with $any', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + expect(result.items).toEqual([entity1, entity3, entity4]); + }); + + it('filters by predicate query with $not', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $all: [ + { kind: 'CustomKind' }, + { $not: { 'spec.lifecycle': 'production' } }, + ], + }, + }); + expect(result.items).toEqual([]); + }); + + it('filters by predicate query with $in', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { 'spec.type': { $in: ['service', 'library'] } }, + }); + expect(result.items).toEqual([entity1, entity2, entity3]); + }); + + it('filters by predicate query with $exists', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { 'spec.lifecycle': { $exists: false } }, + }); + expect(result.items).toEqual([entity4]); + }); + + it('preserves query predicate through cursor pagination', async () => { + const client = new InMemoryCatalogClient({ entities }); + const page1 = await client.queryEntities({ + query: { kind: 'CustomKind' }, + orderFields: { field: 'metadata.name', order: 'asc' }, + limit: 1, + }); + expect(page1.items.map(e => e.metadata.name)).toEqual(['e1']); + expect(page1.totalItems).toBe(2); + expect(page1.pageInfo.nextCursor).toBeDefined(); + + const page2 = await client.queryEntities({ + cursor: page1.pageInfo.nextCursor!, + limit: 1, + }); + expect(page2.items.map(e => e.metadata.name)).toEqual(['e3']); + expect(page2.pageInfo.nextCursor).toBeUndefined(); + }); + it('throws InputError for invalid cursor', async () => { const client = new InMemoryCatalogClient({ entities }); await expect( @@ -898,6 +994,29 @@ describe('InMemoryCatalogClient', () => { }); expect(result.facets['spec.nonexistent']).toEqual([]); }); + + it('supports query predicate filter', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntityFacets({ + facets: ['spec.type'], + query: { kind: 'CustomKind' }, + }); + expect(result.facets['spec.type']).toEqual([ + { value: 'service', count: 2 }, + ]); + }); + + it('supports both filter and query predicate together', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntityFacets({ + facets: ['spec.type'], + filter: { kind: 'CustomKind' }, + query: { 'metadata.namespace': 'default' }, + }); + expect(result.facets['spec.type']).toEqual([ + { value: 'service', count: 1 }, + ]); + }); }); describe('not implemented methods', () => { diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index e64706ff8d..dd7f269188 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -51,6 +51,10 @@ import { NotFoundError, NotImplementedError, } from '@backstage/errors'; +import { + FilterPredicate, + filterPredicateToFilterFunction, +} from '@backstage/filter-predicates'; import lodash from 'lodash'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch'; @@ -357,10 +361,15 @@ export class InMemoryCatalogClient implements CatalogApi { request: GetEntitiesByRefsRequest, ): Promise { const filter = createFilter(request.filter); + const queryFilter = request.query + ? filterPredicateToFilterFunction(request.query) + : undefined; const refMap = this.#createEntityRefMap(); const items = request.entityRefs .map(ref => refMap.get(ref)) - .map(e => (e && filter(e) ? e : undefined)); + .map(e => + e && filter(e) && (!queryFilter || queryFilter(e)) ? e : undefined, + ); return { items: request.fields ? items.map(e => (e ? applyFieldsFilter(e, request.fields) : undefined)) @@ -373,6 +382,7 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise { // Decode query parameters from cursor or from the request directly let filter: EntityFilterQuery | undefined; + let query: FilterPredicate | undefined; let orderFields: EntityOrderQuery | undefined; let fullTextFilter: { term: string; fields?: string[] } | undefined; let offset: number; @@ -386,12 +396,14 @@ export class InMemoryCatalogClient implements CatalogApi { throw new InputError('Invalid cursor'); } filter = deserializeFilter(c.filter as any[]); + query = c.query as FilterPredicate | undefined; orderFields = c.orderFields as EntityOrderQuery | undefined; fullTextFilter = c.fullTextFilter as typeof fullTextFilter; offset = c.offset as number; limit = request.limit; } else { filter = request?.filter; + query = request?.query; orderFields = request?.orderFields; fullTextFilter = request?.fullTextFilter; offset = request?.offset ?? 0; @@ -401,6 +413,11 @@ export class InMemoryCatalogClient implements CatalogApi { // Apply filter let items = this.#entities.filter(createFilter(filter)); + // Apply predicate-based query filter + if (query) { + items = items.filter(filterPredicateToFilterFunction(query)); + } + // Apply full-text filter, defaulting to the sort field or metadata.uid if (fullTextFilter) { const orderFieldsList = orderFields ? [orderFields].flat() : []; @@ -432,6 +449,7 @@ export class InMemoryCatalogClient implements CatalogApi { const cursorBase = { filter: serializeFilter(filter), + query, orderFields, fullTextFilter, totalItems, @@ -493,7 +511,12 @@ export class InMemoryCatalogClient implements CatalogApi { request: GetEntityFacetsRequest, ): Promise { const filter = createFilter(request.filter); - const filteredEntities = this.#entities.filter(filter); + let filteredEntities = this.#entities.filter(filter); + if (request.query) { + filteredEntities = filteredEntities.filter( + filterPredicateToFilterFunction(request.query), + ); + } const facets = Object.fromEntries( request.facets.map(facet => { const facetValues = new Map(); diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 12c5e00c03..d6d5392fcf 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; -import { SerializedError } from '@backstage/errors'; +import type { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import type { SerializedError } from '@backstage/errors'; import type { AnalyzeLocationRequest, AnalyzeLocationResponse, } from '@backstage/plugin-catalog-common'; -import { FilterPredicate } from '@backstage/filter-predicates'; +import type { FilterPredicate } from '@backstage/filter-predicates'; /** * This symbol can be used in place of a value when passed to filters in e.g. - * {@link CatalogClient.getEntities}, to signify that you want to filter on the + * {@link CatalogApi.getEntities}, to signify that you want to filter on the * presence of that key no matter what its value is. * * @public @@ -146,7 +146,7 @@ export type EntityOrderQuery = }>; /** - * The request type for {@link CatalogClient.getEntities}. + * The request type for {@link CatalogApi.getEntities}. * * @public */ @@ -180,7 +180,7 @@ export interface GetEntitiesRequest { } /** - * The response type for {@link CatalogClient.getEntities}. + * The response type for {@link CatalogApi.getEntities}. * * @public */ @@ -189,7 +189,7 @@ export interface GetEntitiesResponse { } /** - * The request type for {@link CatalogClient.getEntitiesByRefs}. + * The request type for {@link CatalogApi.getEntitiesByRefs}. * * @public */ @@ -200,7 +200,7 @@ export interface GetEntitiesByRefsRequest { * @remarks * * The returned list of entities will be in the same order as the refs, and - * null will be returned in those positions that were not found. + * undefined will be returned in those positions that were not found. */ entityRefs: string[]; /** @@ -212,10 +212,20 @@ export interface GetEntitiesByRefsRequest { * If given, return only entities that match the given filter. */ filter?: EntityFilterQuery; + /** + * If given, return only entities that match the given predicate query. + * + * @remarks + * + * Supports operators like `$all`, `$any`, `$not`, `$exists`, `$in`, + * `$contains`, and `$hasPrefix`. When both `filter` and `query` are + * provided, they are combined with `$all`. + */ + query?: FilterPredicate; } /** - * The response type for {@link CatalogClient.getEntitiesByRefs}. + * The response type for {@link CatalogApi.getEntitiesByRefs}. * * @public */ @@ -226,13 +236,13 @@ export interface GetEntitiesByRefsResponse { * @remarks * * The list will be in the same order as the refs given in the request, and - * null will be returned in those positions that were not found. + * undefined will be returned in those positions that were not found. */ items: Array; } /** - * The request type for {@link CatalogClient.getEntityAncestors}. + * The request type for {@link CatalogApi.getEntityAncestors}. * * @public */ @@ -241,7 +251,7 @@ export interface GetEntityAncestorsRequest { } /** - * The response type for {@link CatalogClient.getEntityAncestors}. + * The response type for {@link CatalogApi.getEntityAncestors}. * * @public */ @@ -254,7 +264,7 @@ export interface GetEntityAncestorsResponse { } /** - * The request type for {@link CatalogClient.getEntityFacets}. + * The request type for {@link CatalogApi.getEntityFacets}. * * @public */ @@ -297,6 +307,16 @@ export interface GetEntityFacetsRequest { * of that key, no matter what its value is. */ filter?: EntityFilterQuery; + /** + * If given, return only entities that match the given predicate query. + * + * @remarks + * + * Supports operators like `$all`, `$any`, `$not`, `$exists`, `$in`, + * `$contains`, and `$hasPrefix`. When both `filter` and `query` are + * provided, they are combined with `$all`. + */ + query?: FilterPredicate; /** * Dot separated paths for the facets to extract from each entity. * @@ -323,7 +343,7 @@ export interface GetEntityFacetsRequest { } /** - * The response type for {@link CatalogClient.getEntityFacets}. + * The response type for {@link CatalogApi.getEntityFacets}. * * @public */ @@ -355,7 +375,7 @@ export type Location = { }; /** - * The response type for {@link CatalogClient.getLocations} + * The response type for {@link CatalogApi.getLocations} * * @public */ @@ -364,7 +384,7 @@ export interface GetLocationsResponse { } /** - * The request type for {@link CatalogClient.addLocation}. + * The request type for {@link CatalogApi.addLocation}. * * @public */ @@ -376,10 +396,16 @@ export type AddLocationRequest = { * contain the entities that match the given location. */ dryRun?: boolean; + /** + * Behavior when the location already exists. If set to `'reject'` (the + * default), a conflict error is returned. If set to `'refresh'`, the + * existing location entity is marked for refresh and a 201 is returned. + */ + onConflict?: 'refresh' | 'reject'; }; /** - * The response type for {@link CatalogClient.addLocation}. + * The response type for {@link CatalogApi.addLocation}. * * @public */ @@ -396,7 +422,7 @@ export type AddLocationResponse = { }; /** - * The response type for {@link CatalogClient.validateEntity} + * The response type for {@link CatalogApi.validateEntity} * * @public */ @@ -405,7 +431,7 @@ export type ValidateEntityResponse = | { valid: false; errors: SerializedError[] }; /** - * The request type for {@link CatalogClient.queryEntities}. + * The request type for {@link CatalogApi.queryEntities}. * * @public */ @@ -414,20 +440,47 @@ export type QueryEntitiesRequest = | QueryEntitiesCursorRequest; /** - * A request type for {@link CatalogClient.queryEntities}. + * A request type for {@link CatalogApi.queryEntities}. * The method takes this type in an initial pagination request, * when requesting the first batch of entities. * - * The properties filter, sortField, query and sortFieldOrder, are going + * The properties filter, query, sortField and sortFieldOrder, are going * to be immutable for the entire lifecycle of the following requests. * + * @remarks + * + * Either `filter` or `query` can be provided, or even both: + * - `filter`: Uses the traditional key-value filter syntax (GET endpoint) + * - `query`: Uses the predicate-based filter syntax with logical operators (POST endpoint) + * * @public */ export type QueryEntitiesInitialRequest = { fields?: string[]; limit?: number; offset?: number; + /** + * Traditional key-value based filter. + */ filter?: EntityFilterQuery; + /** + * Predicate-based filter with operators for logical expressions (`$all`, + * `$any`, and `$not`) and matching (`$exists`, `$in`, `$hasPrefix`, and + * (partially) `$contains`). + * + * @example + * ```typescript + * { + * query: { + * $all: [ + * { kind: 'component' }, + * { 'spec.type': { $in: ['service', 'website'] } } + * ] + * } + * } + * ``` + */ + query?: FilterPredicate; orderFields?: EntityOrderQuery; fullTextFilter?: { term: string; @@ -436,7 +489,7 @@ export type QueryEntitiesInitialRequest = { }; /** - * A request type for {@link CatalogClient.queryEntities}. + * A request type for {@link CatalogApi.queryEntities}. * The method takes this type in a pagination request, following * the initial request. * @@ -449,7 +502,7 @@ export type QueryEntitiesCursorRequest = { }; /** - * The response type for {@link CatalogClient.queryEntities}. + * The response type for {@link CatalogApi.queryEntities}. * * @public */ @@ -467,7 +520,7 @@ export type QueryEntitiesResponse = { }; /** - * Stream entities request for {@link CatalogClient.streamEntities}. + * Stream entities request for {@link CatalogApi.streamEntities}. * * @public */ @@ -546,7 +599,7 @@ export interface CatalogApi { * * The output list of entities is of the same size and in the same order as * the requested list of entity refs. Entries that are not found are returned - * as null. + * as undefined. * * @param request - Request parameters * @param options - Additional options @@ -567,6 +620,7 @@ export interface CatalogApi { * const response = await catalogClient.queryEntities({ * filter: [{ kind: 'group' }], * limit: 20, + * fields: ['metadata', 'kind'], * fullTextFilter: { * term: 'A', * }, @@ -583,11 +637,15 @@ export interface CatalogApi { * * ``` * const secondBatchResponse = await catalogClient - * .queryEntities({ cursor: response.nextCursor }); + * .queryEntities({ + * cursor: response.nextCursor, + * limit: 20, + * fields: ['metadata', 'kind'], + * }); * ``` * - * secondBatchResponse will contain the next batch of (maximum) 20 entities, - * together with a prevCursor property, useful to fetch the previous batch. + * `secondBatchResponse` will contain the next batch of (maximum) 20 entities, + * together with a `prevCursor` property, useful to fetch the previous batch. * * @public * diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 19f304b1fd..3755e0d529 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -15,7 +15,9 @@ */ /** - * This is a copy of the DiscoveryApi, to avoid importing core-plugin-api. + * This is a structurally similar version of `DiscoveryApi` / + * `DiscoveryService`, used here to avoid dependencies on the frontend or + * backend plugin API packages and allowing both of those forms to be passed in. */ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; diff --git a/packages/catalog-client/src/types/fetch.ts b/packages/catalog-client/src/types/fetch.ts index 047a9f3558..a99140b749 100644 --- a/packages/catalog-client/src/types/fetch.ts +++ b/packages/catalog-client/src/types/fetch.ts @@ -15,7 +15,9 @@ */ /** - * This is a copy of FetchApi, to avoid importing core-plugin-api. + * This is a structurally similar version of `FetchApi`, used here to avoid + * dependencies on the frontend or backend plugin API packages and allowing both + * of those forms to be passed in. */ export type FetchApi = { fetch: typeof fetch; diff --git a/packages/catalog-client/src/utils.test.ts b/packages/catalog-client/src/utils.test.ts index 2f00859985..ce60cc3083 100644 --- a/packages/catalog-client/src/utils.test.ts +++ b/packages/catalog-client/src/utils.test.ts @@ -14,7 +14,87 @@ * limitations under the License. */ -import { splitRefsIntoChunks } from './utils'; +import { CATALOG_FILTER_EXISTS } from './types/api'; +import { convertFilterToPredicate, splitRefsIntoChunks } from './utils'; + +describe('convertFilterToPredicate', () => { + it('converts a single string value', () => { + expect(convertFilterToPredicate({ kind: 'component' })).toEqual({ + kind: 'component', + }); + }); + + it('converts multiple keys into $all', () => { + expect( + convertFilterToPredicate({ + kind: 'component', + 'spec.type': 'service', + }), + ).toEqual({ + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }); + }); + + it('converts an array of string values into $in', () => { + expect( + convertFilterToPredicate({ 'spec.type': ['service', 'website'] }), + ).toEqual({ + 'spec.type': { $in: ['service', 'website'] }, + }); + }); + + it('converts CATALOG_FILTER_EXISTS into $exists', () => { + expect( + convertFilterToPredicate({ 'spec.owner': CATALOG_FILTER_EXISTS }), + ).toEqual({ + 'spec.owner': { $exists: true }, + }); + }); + + it('converts an array of records into $any (OR)', () => { + expect( + convertFilterToPredicate([{ kind: 'component' }, { kind: 'api' }]), + ).toEqual({ + $any: [{ kind: 'component' }, { kind: 'api' }], + }); + }); + + it('converts array of records with multiple keys each', () => { + expect( + convertFilterToPredicate([ + { kind: 'component', 'spec.type': 'service' }, + { kind: 'api' }, + ]), + ).toEqual({ + $any: [ + { $all: [{ kind: 'component' }, { 'spec.type': 'service' }] }, + { kind: 'api' }, + ], + }); + }); + + it('treats CATALOG_FILTER_EXISTS mixed with string values as just existence', () => { + expect( + convertFilterToPredicate({ + 'spec.owner': [CATALOG_FILTER_EXISTS, 'team-a'], + }), + ).toEqual({ + 'spec.owner': { $exists: true }, + }); + }); + + it('converts a single-element array filter without wrapping in $any', () => { + expect(convertFilterToPredicate([{ kind: 'component' }])).toEqual({ + kind: 'component', + }); + }); + + it('ignores entries with no valid values', () => { + expect( + convertFilterToPredicate({ kind: 'component', other: [] as string[] }), + ).toEqual({ kind: 'component' }); + }); +}); describe('splitRefsIntoChunks', () => { it('splits by count limit', () => { diff --git a/packages/catalog-client/src/utils.ts b/packages/catalog-client/src/utils.ts index a16cf53db1..231aa2409f 100644 --- a/packages/catalog-client/src/utils.ts +++ b/packages/catalog-client/src/utils.ts @@ -14,7 +14,13 @@ * limitations under the License. */ +import type { + FilterPredicate, + FilterPredicateExpression, +} from '@backstage/filter-predicates'; import { + CATALOG_FILTER_EXISTS, + EntityFilterQuery, QueryEntitiesCursorRequest, QueryEntitiesInitialRequest, QueryEntitiesRequest, @@ -26,6 +32,58 @@ export function isQueryEntitiesInitialRequest( return !(request as QueryEntitiesCursorRequest).cursor; } +/** + * Check if a cursor contains a predicate query by attempting to decode it. + * @internal + */ +export function cursorContainsQuery(cursor: string): boolean { + try { + const decoded = JSON.parse(atob(cursor)); + return 'query' in decoded; + } catch { + return false; + } +} + +/** + * Converts an {@link EntityFilterQuery} into a predicate query object. + * @internal + */ +export function convertFilterToPredicate(filter: EntityFilterQuery): + | FilterPredicateExpression + | { + $all: FilterPredicate[]; + } + | { + $any: FilterPredicate[]; + } { + const records = [filter].flat(); + + const clauses = records.map(record => { + const parts: FilterPredicateExpression[] = []; + + for (const [key, value] of Object.entries(record)) { + const values = [value].flat(); + const strings = values.filter((v): v is string => typeof v === 'string'); + const hasExists = values.some(v => v === CATALOG_FILTER_EXISTS); + + if (hasExists) { + // Ignore whether there ALSO were some strings - that would boil down to + // just existence anyway since there's effectively an OR between them + parts.push({ [key]: { $exists: true } } as FilterPredicateExpression); + } else if (strings.length === 1) { + parts.push({ [key]: strings[0] } as FilterPredicateExpression); + } else if (strings.length > 1) { + parts.push({ [key]: { $in: strings } } as FilterPredicateExpression); + } + } + + return parts.length === 1 ? parts[0] : { $all: parts }; + }); + + return clauses.length === 1 ? clauses[0] : { $any: clauses }; +} + /** * Takes a set of entity refs, and splits them into chunks (groups) such that * the total string length in each chunk does not exceed the default Express.js diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index 1f4c794541..d8e264ff71 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/cli-common +## 0.2.0-next.2 + +### Patch Changes + +- 9361965: Fixed `runCheck` to ignore stdio of the spawned process, preventing unwanted output from leaking to the terminal. + +## 0.2.0-next.1 + +### Patch Changes + +- e44b6a9: The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout. +- Updated dependencies + - @backstage/errors@1.2.7 + +## 0.2.0-next.0 + +### Minor Changes + +- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths. + + To migrate existing `findPaths` usage: + + ```ts + // Before + import { findPaths } from '@backstage/cli-common'; + const paths = findPaths(__dirname); + + // After — for target project paths (cwd-based): + import { targetPaths } from '@backstage/cli-common'; + // paths.targetDir → targetPaths.dir + // paths.targetRoot → targetPaths.rootDir + // paths.resolveTarget('src') → targetPaths.resolve('src') + // paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock') + + // After — for package-relative paths: + import { findOwnPaths } from '@backstage/cli-common'; + const own = findOwnPaths(__dirname); + // paths.ownDir → own.dir + // paths.ownRoot → own.rootDir + // paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js') + // paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json') + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.7 + +## 0.1.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + ## 0.1.18-next.0 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index f2bd356a73..a4e866dd37 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,14 +1,12 @@ { "name": "@backstage/cli-common", - "version": "0.1.18-next.0", + "version": "0.2.0-next.2", "description": "Common functionality used by cli, backend, and create-app", "backstage": { "role": "node-library" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage" @@ -20,8 +18,23 @@ "directory": "packages/cli-common" }, "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./testUtils": "./src/testUtils.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "testUtils": [ + "src/testUtils.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], diff --git a/packages/cli-common/report-testUtils.api.md b/packages/cli-common/report-testUtils.api.md new file mode 100644 index 0000000000..012803d1fb --- /dev/null +++ b/packages/cli-common/report-testUtils.api.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/cli-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export function overrideTargetPaths( + dirOrOptions: string | OverrideTargetPathsOptions, +): TargetPathsOverride; + +// @public +export interface OverrideTargetPathsOptions { + dir: string; + rootDir?: string; +} + +// @public +export interface TargetPathsOverride { + restore(): void; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index 96475c6bd1..ba649cacc9 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -16,17 +16,27 @@ export function bootstrapEnvProxyAgents(): void; // @public export class ExitCodeError extends CustomErrorBase { constructor(code: number, command?: string); - // (undocumented) readonly code: number; } // @public +export function findOwnPaths(searchDir: string): OwnPaths; + +// @public @deprecated export function findPaths(searchDir: string): Paths; // @public export function isChildPath(base: string, path: string): boolean; // @public +export type OwnPaths = { + dir: string; + rootDir: string; + resolve: ResolveFunc; + resolveRoot: ResolveFunc; +}; + +// @public @deprecated export type Paths = { ownDir: string; ownRoot: string; @@ -68,4 +78,15 @@ export function runOutput( args: string[], options?: RunOptions, ): Promise; + +// @public +export type TargetPaths = { + dir: string; + rootDir: string; + resolve: ResolveFunc; + resolveRoot: ResolveFunc; +}; + +// @public +export const targetPaths: TargetPaths; ``` diff --git a/packages/cli-common/src/errors.ts b/packages/cli-common/src/errors.ts index 07664d2ffb..c684ac68d5 100644 --- a/packages/cli-common/src/errors.ts +++ b/packages/cli-common/src/errors.ts @@ -21,6 +21,7 @@ import { CustomErrorBase } from '@backstage/errors'; * @public */ export class ExitCodeError extends CustomErrorBase { + /** The exit code of the child process. */ readonly code: number; constructor(code: number, command?: string) { diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index d11601b044..e49eac0ee0 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -20,9 +20,9 @@ * @packageDocumentation */ -export { findPaths, BACKSTAGE_JSON } from './paths'; +export { findPaths, findOwnPaths, targetPaths, BACKSTAGE_JSON } from './paths'; export { isChildPath } from './isChildPath'; -export type { Paths, ResolveFunc } from './paths'; +export type { Paths, TargetPaths, OwnPaths, ResolveFunc } from './paths'; export { bootstrapEnvProxyAgents } from './proxyBootstrap'; export { run, diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 6ade904e12..df0e7c2044 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -16,18 +16,18 @@ /* eslint-disable no-restricted-syntax */ import { resolve as resolvePath } from 'node:path'; -import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths'; +import { findPaths, findRootPath, findOwnRootDir, findOwnPaths } from './paths'; describe('paths', () => { afterEach(() => { jest.restoreAllMocks(); }); - it('findOwnDir and findOwnRootDir should find owns paths', () => { - const dir = findOwnDir(__dirname); - const root = findOwnRootDir(dir); + it('findOwnPaths and findOwnRootDir should find own paths', () => { + const own = findOwnPaths(__dirname); + const root = findOwnRootDir(own.dir); - expect(dir).toBe(resolvePath(__dirname, '..')); + expect(own.dir).toBe(resolvePath(__dirname, '..')); expect(root).toBe(resolvePath(__dirname, '../../..')); }); @@ -98,7 +98,9 @@ describe('paths', () => { }); it('findPaths should find workspace root with object', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } }); + jest + .spyOn(JSON, 'parse') + .mockReturnValue({ workspaces: { packages: ['packages/*'] } }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); @@ -110,7 +112,7 @@ describe('paths', () => { }); it('findPaths should find workspace root with array', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] }); + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: ['packages/*'] }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 5da2d596a4..c6bedc77e4 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -25,35 +25,61 @@ import { dirname, resolve as resolvePath } from 'node:path'; */ export type ResolveFunc = (...paths: string[]) => string; +/** + * Resolved paths relative to the target project, based on `process.cwd()`. + * Lazily initialized on first property access. Re-resolves automatically + * when `process.cwd()` changes. + * + * @public + */ +export type TargetPaths = { + /** The target package directory. */ + dir: string; + + /** The target monorepo root directory. */ + rootDir: string; + + /** Resolve a path relative to the target package directory. */ + resolve: ResolveFunc; + + /** Resolve a path relative to the target repo root. */ + resolveRoot: ResolveFunc; +}; + +/** + * Resolved paths relative to a specific package in the repository. + * + * @public + */ +export type OwnPaths = { + /** The package root directory. */ + dir: string; + + /** The monorepo root directory containing the package. */ + rootDir: string; + + /** Resolve a path relative to the package root. */ + resolve: ResolveFunc; + + /** Resolve a path relative to the monorepo root containing the package. */ + resolveRoot: ResolveFunc; +}; + /** * Common paths and resolve functions used by the cli. * Currently assumes it is being executed within a monorepo. * * @public + * @deprecated Use {@link targetPaths} and {@link findOwnPaths} instead. */ export type Paths = { - // Root dir of the cli itself, containing package.json ownDir: string; - - // Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo. ownRoot: string; - - // The location of the app that the cli is being executed in targetDir: string; - - // The monorepo root package of the app that the cli is being executed in. targetRoot: string; - - // Resolve a path relative to own repo resolveOwn: ResolveFunc; - - // Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo. resolveOwnRoot: ResolveFunc; - - // Resolve a path relative to the app resolveTarget: ResolveFunc; - - // Resolve a path relative to the app repo root resolveTargetRoot: ResolveFunc; }; @@ -84,18 +110,7 @@ export function findRootPath( ); } -// Finds the root of a given package -export function findOwnDir(searchDir: string) { - const path = findRootPath(searchDir, () => true); - if (!path) { - throw new Error( - `No package.json found while searching for package root of ${searchDir}`, - ); - } - return path; -} - -// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo. +// Finds the root of the monorepo that the package exists in. export function findOwnRootDir(ownDir: string) { const isLocal = fs.existsSync(resolvePath(ownDir, 'src')); if (!isLocal) { @@ -104,67 +119,223 @@ export function findOwnRootDir(ownDir: string) { ); } - return resolvePath(ownDir, '../..'); + const rootDir = findRootPath(ownDir, pkgJsonPath => { + try { + const content = fs.readFileSync(pkgJsonPath, 'utf8'); + const data = JSON.parse(content); + return Boolean(data.workspaces); + } catch (error) { + throw new Error( + `Failed to read package.json at '${pkgJsonPath}', ${error}`, + ); + } + }); + + if (!rootDir) { + throw new Error(`No monorepo root found when searching from '${ownDir}'`); + } + + return rootDir; +} + +// Hierarchical directory cache shared across all OwnPathsImpl instances. +// When we resolve a searchDir to its package root, we also cache every +// intermediate directory, so sibling directories share work. +const dirCache = new Map(); + +class OwnPathsImpl implements OwnPaths { + static #instanceCache = new Map(); + + static find(searchDir: string): OwnPathsImpl { + const dir = OwnPathsImpl.findDir(searchDir); + let instance = OwnPathsImpl.#instanceCache.get(dir); + if (!instance) { + instance = new OwnPathsImpl(dir); + OwnPathsImpl.#instanceCache.set(dir, instance); + } + return instance; + } + + static findDir(searchDir: string): string { + const visited: string[] = []; + let dir = searchDir; + + for (let i = 0; i < 1000; i++) { + const cached = dirCache.get(dir); + if (cached !== undefined) { + for (const d of visited) { + dirCache.set(d, cached); + } + return cached; + } + + visited.push(dir); + + if (fs.existsSync(resolvePath(dir, 'package.json'))) { + for (const d of visited) { + dirCache.set(d, dir); + } + return dir; + } + + const newDir = dirname(dir); + if (newDir === dir) { + break; + } + dir = newDir; + } + + throw new Error( + `No package.json found while searching for package root of ${searchDir}`, + ); + } + + #dir: string; + #rootDir: string | undefined; + + private constructor(dir: string) { + this.#dir = dir; + } + + get dir(): string { + return this.#dir; + } + + get rootDir(): string { + this.#rootDir ??= findOwnRootDir(this.#dir); + return this.#rootDir; + } + + resolve = (...paths: string[]): string => { + return resolvePath(this.#dir, ...paths); + }; + + resolveRoot = (...paths: string[]): string => { + return resolvePath(this.rootDir, ...paths); + }; +} + +// Used by the test utility in testUtils.ts to override targetPaths +export let targetPathsOverride: TargetPaths | undefined; + +/** @internal */ +export function setTargetPathsOverride(override: TargetPaths | undefined) { + targetPathsOverride = override; +} + +class TargetPathsImpl implements TargetPaths { + #cwd: string | undefined; + #dir: string | undefined; + #rootDir: string | undefined; + + get dir(): string { + if (targetPathsOverride) { + return targetPathsOverride.dir; + } + const cwd = process.cwd(); + if (this.#dir !== undefined && this.#cwd === cwd) { + return this.#dir; + } + this.#cwd = cwd; + this.#rootDir = undefined; + // Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency + this.#dir = fs + .realpathSync(cwd) + .replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US')); + return this.#dir; + } + + get rootDir(): string { + if (targetPathsOverride) { + return targetPathsOverride.rootDir; + } + // Access dir first to ensure cwd is fresh, which also invalidates rootDir on cwd change + const dir = this.dir; + if (this.#rootDir !== undefined) { + return this.#rootDir; + } + // Lazy init to only crash commands that require a monorepo when we're not in one + this.#rootDir = + findRootPath(dir, path => { + try { + const content = fs.readFileSync(path, 'utf8'); + const data = JSON.parse(content); + return Boolean(data.workspaces); + } catch (error) { + throw new Error( + `Failed to parse package.json file while searching for root, ${error}`, + ); + } + }) ?? dir; + return this.#rootDir; + } + + resolve = (...paths: string[]): string => { + if (targetPathsOverride) { + return targetPathsOverride.resolve(...paths); + } + return resolvePath(this.dir, ...paths); + }; + + resolveRoot = (...paths: string[]): string => { + if (targetPathsOverride) { + return targetPathsOverride.resolveRoot(...paths); + } + return resolvePath(this.rootDir, ...paths); + }; +} + +/** + * Lazily resolved paths relative to the target project. Import this directly + * for cwd-based path resolution without needing `__dirname`. + * + * @public + */ +export const targetPaths: TargetPaths = new TargetPathsImpl(); + +/** + * Find paths relative to the package that the calling code lives in. + * + * Results are cached per package root, and the package root lookup uses a + * hierarchical directory cache so that multiple calls from different + * subdirectories within the same package share work. + * + * @public + */ +export function findOwnPaths(searchDir: string): OwnPaths { + return OwnPathsImpl.find(searchDir); } /** * Find paths related to a package and its execution context. * * @public + * @deprecated Use {@link targetPaths} for cwd-based paths and + * {@link findOwnPaths} for package-relative paths instead. + * * @example * * const paths = findPaths(__dirname) */ export function findPaths(searchDir: string): Paths { - const ownDir = findOwnDir(searchDir); - // Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency - const targetDir = fs - .realpathSync(process.cwd()) - .replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US')); - - // Lazy load this as it will throw an error if we're not inside the Backstage repo. - let ownRoot = ''; - const getOwnRoot = () => { - if (!ownRoot) { - ownRoot = findOwnRootDir(ownDir); - } - return ownRoot; - }; - - // We're not always running in a monorepo, so we lazy init this to only crash commands - // that require a monorepo when we're not in one. - let targetRoot = ''; - const getTargetRoot = () => { - if (!targetRoot) { - targetRoot = - findRootPath(targetDir, path => { - try { - const content = fs.readFileSync(path, 'utf8'); - const data = JSON.parse(content); - return Boolean(data.workspaces); - } catch (error) { - throw new Error( - `Failed to parse package.json file while searching for root, ${error}`, - ); - } - }) ?? targetDir; // We didn't find any root package.json, assume we're not in a monorepo - } - return targetRoot; - }; - + const own = findOwnPaths(searchDir); return { - ownDir, + get ownDir() { + return own.dir; + }, get ownRoot() { - return getOwnRoot(); + return own.rootDir; + }, + get targetDir() { + return targetPaths.dir; }, - targetDir, get targetRoot() { - return getTargetRoot(); + return targetPaths.rootDir; }, - resolveOwn: (...paths) => resolvePath(ownDir, ...paths), - resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths), - resolveTarget: (...paths) => resolvePath(targetDir, ...paths), - resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), + resolveOwn: own.resolve, + resolveOwnRoot: own.resolveRoot, + resolveTarget: targetPaths.resolve, + resolveTargetRoot: targetPaths.resolveRoot, }; } diff --git a/packages/cli-common/src/run.test.ts b/packages/cli-common/src/run.test.ts index 01c6c915d6..f38c1659ba 100644 --- a/packages/cli-common/src/run.test.ts +++ b/packages/cli-common/src/run.test.ts @@ -338,5 +338,26 @@ describe('run', () => { const result = await runCheck(['nonexistent-command-12345']); expect(result).toBe(false); }); + + it('should not leak stdout or stderr from the child process', async () => { + const stdoutSpy = jest.spyOn(process.stdout, 'write'); + const stderrSpy = jest.spyOn(process.stderr, 'write'); + const stdoutBefore = stdoutSpy.mock.calls.length; + const stderrBefore = stderrSpy.mock.calls.length; + + await runCheck([ + 'node', + '--eval', + 'console.log("leaked stdout"); console.error("leaked stderr")', + ]); + + const stdoutCalls = stdoutSpy.mock.calls.slice(stdoutBefore); + const stderrCalls = stderrSpy.mock.calls.slice(stderrBefore); + const stdout = stdoutCalls.map(c => String(c[0])).join(''); + const stderr = stderrCalls.map(c => String(c[0])).join(''); + + expect(stdout).not.toContain('leaked stdout'); + expect(stderr).not.toContain('leaked stderr'); + }); }); }); diff --git a/packages/cli-common/src/run.ts b/packages/cli-common/src/run.ts index 23e71460e4..0e95bd517b 100644 --- a/packages/cli-common/src/run.ts +++ b/packages/cli-common/src/run.ts @@ -211,7 +211,7 @@ export async function runOutput( */ export async function runCheck(args: string[]): Promise { try { - await run(args).waitForExit(); + await run(args, { stdio: 'ignore' }).waitForExit(); return true; } catch { return false; diff --git a/packages/cli-common/src/testUtils.ts b/packages/cli-common/src/testUtils.ts new file mode 100644 index 0000000000..87c42c469e --- /dev/null +++ b/packages/cli-common/src/testUtils.ts @@ -0,0 +1,78 @@ +/* + * 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 { resolve as resolvePath } from 'node:path'; +import { setTargetPathsOverride } from './paths'; + +/** + * Options for {@link overrideTargetPaths}. + * + * @public + */ +export interface OverrideTargetPathsOptions { + /** The target package directory. */ + dir: string; + /** The target monorepo root directory. Defaults to `dir` if not provided. */ + rootDir?: string; +} + +/** + * Return value of {@link overrideTargetPaths}. + * + * @public + */ +export interface TargetPathsOverride { + /** Restores `targetPaths` to its normal behavior. */ + restore(): void; +} + +/** + * Overrides the `targetPaths` singleton to resolve from the given directory + * instead of `process.cwd()`. + * + * When called with a string, that value is used as both `dir` and `rootDir`. + * Pass an options object to set them independently. + * + * Calling `restore()` on the return value reverts to normal behavior. + * Restoration is only needed if you want to change the override within a + * test file; each Jest worker starts with a clean module state. + * + * @public + */ +export function overrideTargetPaths( + dirOrOptions: string | OverrideTargetPathsOptions, +): TargetPathsOverride { + const { dir, rootDir } = + typeof dirOrOptions === 'string' + ? { dir: dirOrOptions, rootDir: dirOrOptions } + : { + dir: dirOrOptions.dir, + rootDir: dirOrOptions.rootDir ?? dirOrOptions.dir, + }; + + setTargetPathsOverride({ + dir, + rootDir, + resolve: (...paths: string[]) => resolvePath(dir, ...paths), + resolveRoot: (...paths: string[]) => resolvePath(rootDir, ...paths), + }); + + return { + restore() { + setTargetPathsOverride(undefined); + }, + }; +} diff --git a/plugins/scaffolder-backend-module-bitbucket/.eslintrc.js b/packages/cli-defaults/.eslintrc.js similarity index 100% rename from plugins/scaffolder-backend-module-bitbucket/.eslintrc.js rename to packages/cli-defaults/.eslintrc.js diff --git a/packages/cli-defaults/README.md b/packages/cli-defaults/README.md new file mode 100644 index 0000000000..2066aa1e35 --- /dev/null +++ b/packages/cli-defaults/README.md @@ -0,0 +1,26 @@ +# @backstage/cli-defaults + +The default set of CLI modules for the Backstage CLI. Installing this single package provides all standard CLI commands without needing to list each module individually. + +## Included Modules + +| Module | Description | +| :----------------------------------------------------------------- | :--------------------------------------- | +| [`@backstage/cli-module-auth`](../cli-module-auth) | Authentication commands | +| [`@backstage/cli-module-build`](../cli-module-build) | Build, start, and packaging commands | +| [`@backstage/cli-module-config`](../cli-module-config) | Configuration inspection commands | +| [`@backstage/cli-module-github`](../cli-module-github) | GitHub App creation | +| [`@backstage/cli-module-info`](../cli-module-info) | Environment and dependency info | +| [`@backstage/cli-module-lint`](../cli-module-lint) | Linting commands | +| [`@backstage/cli-module-maintenance`](../cli-module-maintenance) | Repository maintenance commands | +| [`@backstage/cli-module-migrate`](../cli-module-migrate) | Migration and version management | +| [`@backstage/cli-module-new`](../cli-module-new) | Scaffolding for new plugins and packages | +| [`@backstage/cli-module-test-jest`](../cli-module-test-jest) | Jest-based testing commands | +| [`@backstage/cli-module-translations`](../cli-module-translations) | Translation management commands | + +For fine-grained control over which CLI commands are available, you can install individual modules instead. + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-defaults/catalog-info.yaml b/packages/cli-defaults/catalog-info.yaml new file mode 100644 index 0000000000..3cac883c36 --- /dev/null +++ b/packages/cli-defaults/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-defaults + title: '@backstage/cli-defaults' + description: Default set of CLI modules for the Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-defaults/package.json b/packages/cli-defaults/package.json new file mode 100644 index 0000000000..8a065fd4d7 --- /dev/null +++ b/packages/cli-defaults/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/cli-defaults", + "version": "0.0.0", + "description": "Default set of CLI modules for the Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-defaults" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@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:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-defaults/report.api.md b/packages/cli-defaults/report.api.md new file mode 100644 index 0000000000..573f8ee454 --- /dev/null +++ b/packages/cli-defaults/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public +const _default: CliModule[]; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-defaults/src/index.ts b/packages/cli-defaults/src/index.ts new file mode 100644 index 0000000000..dd7aed0c70 --- /dev/null +++ b/packages/cli-defaults/src/index.ts @@ -0,0 +1,45 @@ +/* + * 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 auth from '@backstage/cli-module-auth'; +import build from '@backstage/cli-module-build'; +import config from '@backstage/cli-module-config'; +import github from '@backstage/cli-module-github'; +import info from '@backstage/cli-module-info'; +import lint from '@backstage/cli-module-lint'; +import maintenance from '@backstage/cli-module-maintenance'; +import migrate from '@backstage/cli-module-migrate'; +import newModule from '@backstage/cli-module-new'; +import testJest from '@backstage/cli-module-test-jest'; +import translations from '@backstage/cli-module-translations'; + +/** + * The default set of CLI modules for the Backstage CLI. + * + * @public + */ +export default [ + auth, + build, + config, + github, + info, + lint, + maintenance, + migrate, + newModule, + testJest, + translations, +]; diff --git a/packages/cli-internal/.eslintrc.js b/packages/cli-internal/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-internal/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-internal/catalog-info.yaml b/packages/cli-internal/catalog-info.yaml new file mode 100644 index 0000000000..2f2a1c837a --- /dev/null +++ b/packages/cli-internal/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: internal-cli + title: '@internal/cli' +spec: + lifecycle: experimental + type: backstage-node-library + owner: tooling-maintainers diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json new file mode 100644 index 0000000000..068735bac3 --- /dev/null +++ b/packages/cli-internal/package.json @@ -0,0 +1,30 @@ +{ + "name": "@internal/cli", + "version": "0.0.1", + "backstage": { + "role": "node-library", + "inline": true + }, + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-internal" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-node": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-internal/src/InternalCliModule.ts b/packages/cli-internal/src/InternalCliModule.ts new file mode 100644 index 0000000000..213e309371 --- /dev/null +++ b/packages/cli-internal/src/InternalCliModule.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 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 { CliCommand, CliModule } from '@backstage/cli-node'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueCliModule = OpaqueType.create<{ + public: CliModule; + versions: { + readonly version: 'v1'; + readonly packageName: string; + readonly commands: Promise>; + }; +}>({ + type: '@backstage/CliModule', + versions: ['v1'], +}); diff --git a/packages/cli-internal/src/InternalCommandNode.ts b/packages/cli-internal/src/InternalCommandNode.ts new file mode 100644 index 0000000000..233e9128cb --- /dev/null +++ b/packages/cli-internal/src/InternalCommandNode.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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 { CliCommand, CliModule } from '@backstage/cli-node'; +import { OpaqueType } from '@internal/opaque'; + +/** @internal */ +export interface CommandTreeNode { + readonly $$type: '@backstage/CommandTreeNode'; +} + +/** @internal */ +export interface CommandLeafNode { + readonly $$type: '@backstage/CommandLeafNode'; +} + +export type CommandNode = CommandTreeNode | CommandLeafNode; + +export const OpaqueCommandTreeNode = OpaqueType.create<{ + public: CommandTreeNode; + versions: { + readonly version: 'v1'; + readonly name: string; + readonly children: CommandNode[]; + }; +}>({ + type: '@backstage/CommandTreeNode', + versions: ['v1'], +}); + +export const OpaqueCommandLeafNode = OpaqueType.create<{ + public: CommandLeafNode; + versions: { + readonly version: 'v1'; + readonly name: string; + readonly command: CliCommand; + readonly module?: CliModule; + }; +}>({ + type: '@backstage/CommandLeafNode', + versions: ['v1'], +}); + +/** + * Checks whether a command node should be hidden from help output. + * Leaf nodes are hidden if they are deprecated or experimental. + * Tree nodes are hidden if all of their children are hidden. + */ +export function isCommandNodeHidden(node: CommandNode): boolean { + if (OpaqueCommandLeafNode.isType(node)) { + const { command } = OpaqueCommandLeafNode.toInternal(node); + return !!command.deprecated || !!command.experimental; + } + const { children } = OpaqueCommandTreeNode.toInternal(node); + return children.every(child => isCommandNodeHidden(child)); +} diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts new file mode 100644 index 0000000000..0e69d45032 --- /dev/null +++ b/packages/cli-internal/src/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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 { OpaqueCliModule } from './InternalCliModule'; +export type { + CommandNode, + CommandTreeNode, + CommandLeafNode, +} from './InternalCommandNode'; +export { + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from './InternalCommandNode'; diff --git a/packages/cli-module-auth/.eslintrc.js b/packages/cli-module-auth/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-auth/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-auth/README.md b/packages/cli-module-auth/README.md new file mode 100644 index 0000000000..2a98761ac3 --- /dev/null +++ b/packages/cli-module-auth/README.md @@ -0,0 +1,19 @@ +# @backstage/cli-module-auth + +CLI module that provides authentication commands for the Backstage CLI, enabling login, logout, and credential management for Backstage instances. + +## Commands + +| Command | Description | +| :----------------- | :------------------------------------------------------- | +| `auth login` | Log in the CLI to a Backstage instance | +| `auth logout` | Log out the CLI and clear stored credentials | +| `auth show` | Show details of an authenticated instance | +| `auth list` | List authenticated instances | +| `auth print-token` | Print an access token to stdout (auto-refresh if needed) | +| `auth select` | Select the default instance | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli/src/modules/migrate/commands/packageExports.ts b/packages/cli-module-auth/bin/backstage-cli-module-auth old mode 100644 new mode 100755 similarity index 51% rename from packages/cli/src/modules/migrate/commands/packageExports.ts rename to packages/cli-module-auth/bin/backstage-cli-module-auth index 4eb741ce4d..68bdc76a4a --- a/packages/cli/src/modules/migrate/commands/packageExports.ts +++ b/packages/cli-module-auth/bin/backstage-cli-module-auth @@ -1,5 +1,6 @@ +#!/usr/bin/env node /* - * Copyright 2020 The Backstage Authors + * Copyright 2024 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. @@ -14,21 +15,18 @@ * limitations under the License. */ -import { - fixPackageExports, - readFixablePackages, - writeFixedPackages, -} from '../../maintenance/commands/repo/fix'; +const path = require('node:path'); -export async function command() { - console.log( - 'The `migrate package-exports` command is deprecated, use `repo fix` instead.', - ); - const packages = await readFixablePackages(); +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); - for (const pkg of packages) { - fixPackageExports(pkg); - } - - await writeFixedPackages(packages); +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); } + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-auth/catalog-info.yaml b/packages/cli-module-auth/catalog-info.yaml new file mode 100644 index 0000000000..091bcad199 --- /dev/null +++ b/packages/cli-module-auth/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-auth + title: '@backstage/cli-module-auth' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-auth/cli-report.md b/packages/cli-module-auth/cli-report.md new file mode 100644 index 0000000000..8b4c98766f --- /dev/null +++ b/packages/cli-module-auth/cli-report.md @@ -0,0 +1,96 @@ +## CLI Report file for "@backstage/cli-module-auth" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-auth` + +``` +Usage: @backstage/cli-module-auth [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + auth [command] + help [command] +``` + +### `backstage-cli-module-auth auth` + +``` +Usage: @backstage/cli-module-auth auth [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + list + login + logout + print-token + select + show +``` + +### `backstage-cli-module-auth auth list` + +``` +Usage: @backstage/cli-module-auth auth list + +Options: + -h, --help +``` + +### `backstage-cli-module-auth auth login` + +``` +Usage: @backstage/cli-module-auth auth login + +Options: + --backend-url + --instance + --no-browser + -h, --help +``` + +### `backstage-cli-module-auth auth logout` + +``` +Usage: @backstage/cli-module-auth auth logout + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth print-token` + +``` +Usage: @backstage/cli-module-auth auth print-token + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth select` + +``` +Usage: @backstage/cli-module-auth auth select + +Options: + --instance + -h, --help +``` + +### `backstage-cli-module-auth auth show` + +``` +Usage: @backstage/cli-module-auth auth show + +Options: + --instance + -h, --help +``` diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json new file mode 100644 index 0000000000..86bb491f7a --- /dev/null +++ b/packages/cli-module-auth/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/cli-module-auth", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-auth" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "cleye": "^2.3.0", + "cross-fetch": "^4.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "inquirer": "^8.2.0", + "proper-lockfile": "^4.1.2", + "yaml": "^2.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/proper-lockfile": "^4" + }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, + "bin": "bin/backstage-cli-module-auth" +} diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md new file mode 100644 index 0000000000..510bcaabe7 --- /dev/null +++ b/packages/cli-module-auth/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-auth/src/commands/list.ts b/packages/cli-module-auth/src/commands/list.ts new file mode 100644 index 0000000000..3da2118800 --- /dev/null +++ b/packages/cli-module-auth/src/commands/list.ts @@ -0,0 +1,33 @@ +/* + * 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 { cli } from 'cleye'; +import type { CliCommandContext } from '@backstage/cli-node'; +import { getAllInstances } from '../lib/storage'; + +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info }, undefined, args); + + const { instances, selected } = await getAllInstances(); + if (!instances.length) { + process.stderr.write('No instances found\n'); + return; + } + for (const inst of instances) { + const mark = inst.name === selected?.name ? '* ' : ' '; + process.stdout.write(`${mark}${inst.name} - ${inst.baseUrl}\n`); + } +}; diff --git a/packages/cli-module-auth/src/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts new file mode 100644 index 0000000000..b16060f956 --- /dev/null +++ b/packages/cli-module-auth/src/commands/login.ts @@ -0,0 +1,383 @@ +/* + * 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 { cli } from 'cleye'; +import type { CliCommandContext } from '@backstage/cli-node'; +import { startCallbackServer } from '../lib/localServer'; +import { spawn } from 'node:child_process'; +import { challengeFromVerifier, generateVerifier } from '../lib/pkce'; +import { httpJson } from '../lib/http'; +import { + upsertInstance, + withMetadataLock, + getAllInstances, + getInstanceByName, + StoredInstance, +} from '../lib/storage'; +import { getSecretStore } from '../lib/secretStore'; +import crypto from 'node:crypto'; +import fs from 'fs-extra'; +import path from 'node:path'; +import glob from 'glob'; +import YAML from 'yaml'; +import inquirer from 'inquirer'; + +const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { backendUrl, noBrowser, instance: instanceFlag }, + } = cli( + { + help: info, + flags: { + backendUrl: { type: String, description: 'Backend base URL' }, + noBrowser: { + type: Boolean, + description: 'Do not open browser automatically', + }, + instance: { + type: String, + description: 'Name for this instance (used by other auth commands)', + }, + }, + }, + undefined, + args, + ); + + const { instances, selected } = await getAllInstances(); + + let backendBaseUrl: string; + let instanceName: string; + + if (instanceFlag) { + instanceName = instanceFlag; + const targetInstance = instances.find(i => i.name === instanceFlag); + if (targetInstance) { + backendBaseUrl = normalizeUrl(backendUrl) ?? targetInstance.baseUrl; + } else { + backendBaseUrl = normalizeUrl(backendUrl) ?? (await pickBaseUrl()); + } + } else if (backendUrl) { + backendBaseUrl = normalizeUrl(backendUrl); + instanceName = deriveInstanceName(backendBaseUrl); + } else if (instances.length > 0) { + const choice = await promptForInstance(instances, selected); + if (choice === '__new__') { + backendBaseUrl = await pickBaseUrl(); + instanceName = deriveInstanceName(backendBaseUrl); + } else { + const targetInstance = instances.find(i => i.name === choice); + if (!targetInstance) { + throw new Error('Instance not found'); + } + backendBaseUrl = targetInstance.baseUrl; + instanceName = targetInstance.name; + } + } else { + backendBaseUrl = await pickBaseUrl(); + instanceName = deriveInstanceName(backendBaseUrl); + } + + const authBaseUrl = `${backendBaseUrl}/api/auth`; + const clientId = `${authBaseUrl}/.well-known/oauth-client/cli.json`; + + const metadataResponse = await fetch(clientId, { + signal: AbortSignal.timeout(30_000), + }); + if (!metadataResponse.ok) { + throw new Error( + `Server does not support CLI authentication. Ensure CIMD is enabled on the backend.`, + ); + } + + const { verifier, challenge, state } = createPkceState(); + const callback = await startCallbackServer({ state }); + + try { + const authorizeUrl = buildAuthorizeUrl({ + authBaseUrl, + clientId, + redirectUri: callback.url, + state, + challenge, + }); + + await openBrowserOrPrint(authorizeUrl, noBrowser); + + const code = await waitForAuthorizationCode(callback, state); + + const token = await exchangeAuthorizationCode({ + authBaseUrl, + code, + redirectUri: callback.url, + verifier, + }); + + await persistInstance({ + instanceName, + backendBaseUrl, + clientId, + token, + }); + + process.stdout.write('Login successful\n'); + } finally { + await callback.close(); + } +}; + +async function promptForInstance( + instances: StoredInstance[], + selected: StoredInstance | undefined, +): Promise { + const choices = instances.map(i => ({ + name: `${i.name === selected?.name ? '* ' : ' '}${i.name} (${i.baseUrl})`, + value: i.name, + })); + + choices.push({ + name: 'Add new instance...', + value: '__new__', + }); + + const { choice } = await inquirer.prompt<{ choice: string }>([ + { + type: 'list', + name: 'choice', + message: 'Select instance to authenticate:', + choices, + default: selected?.name ?? '__new__', + }, + ]); + + return choice; +} + +async function pickBaseUrl() { + const cwd = process.cwd(); + const candidates: Array<{ url: string; file: string }> = []; + + const patterns = [ + 'app-config.yaml', + 'app-config.*.yaml', + 'packages/*/app-config.yaml', + 'packages/*/app-config.*.yaml', + ]; + const files = patterns.flatMap(p => glob.sync(p, { cwd, nodir: true })); + for (const file of files) { + try { + const content = await fs.readFile(path.resolve(cwd, file), 'utf8'); + const doc = YAML.parse(content); + const url = doc?.backend?.baseUrl as string | undefined; + if (url) { + candidates.push({ url: normalizeUrl(url), file }); + } + } catch { + // ignore parse errors + } + } + + const list = [...new Map(candidates.map(c => [c.url, c])).values()]; + if (list.length === 0) { + const { manual } = await inquirer.prompt<{ manual: string }>([ + { type: 'input', name: 'manual', message: 'Enter backend base URL' }, + ]); + return normalizeUrl(manual); + } + if (list.length === 1) { + return list[0].url; + } + + const { picked } = await inquirer.prompt<{ picked: string }>([ + { + type: 'list', + name: 'picked', + message: 'Select backend base URL', + choices: [ + ...list.map(e => ({ name: `${e.url} (${e.file})`, value: e.url })), + { name: 'Enter manually', value: '__manual__' }, + ], + }, + ]); + if (picked === '__manual__') { + const { manual } = await inquirer.prompt<{ manual: string }>([ + { type: 'input', name: 'manual', message: 'Enter backend base URL' }, + ]); + return normalizeUrl(manual); + } + return picked; +} + +function normalizeUrl(u: string): string; +function normalizeUrl(u: string | undefined): string | undefined; +function normalizeUrl(u: string | undefined): string | undefined { + if (u === undefined) { + return undefined; + } + try { + const url = new URL(u); + return url.toString().replace(/\/$/, ''); + } catch { + throw new Error(`'${u}' is not a valid URL`); + } +} + +function deriveInstanceName(url: string): string { + return new URL(url).host; +} + +function createPkceState() { + const verifier = generateVerifier(); + const challenge = challengeFromVerifier(verifier); + const state = cryptoRandom(); + return { verifier, challenge, state }; +} + +function buildAuthorizeUrl(options: { + authBaseUrl: string; + clientId: string; + redirectUri: string; + state: string; + challenge: string; +}): string { + const { authBaseUrl, clientId, redirectUri, state, challenge } = options; + const authorize = new URL(`${authBaseUrl}/v1/authorize`); + authorize.searchParams.set('client_id', clientId); + authorize.searchParams.set('redirect_uri', redirectUri); + authorize.searchParams.set('response_type', 'code'); + authorize.searchParams.set('scope', 'openid offline_access'); + authorize.searchParams.set('state', state); + authorize.searchParams.set('code_challenge', challenge); + authorize.searchParams.set('code_challenge_method', 'S256'); + return authorize.toString(); +} + +async function openBrowserOrPrint(url: string, noBrowser?: boolean) { + if (noBrowser) { + process.stdout.write(`Open this URL to continue: ${url}\n`); + } else { + process.stdout.write(`Opening the following URL: ${url}\n`); + openInBrowser(url); + } +} + +async function waitForAuthorizationCode( + callback: Awaited>, + expectedState: string, +) { + const { code, state } = await callback.waitForCode(); + if (state !== expectedState) { + throw new Error('State mismatch'); + } + return code; +} + +async function exchangeAuthorizationCode(options: { + authBaseUrl: string; + code: string; + redirectUri: string; + verifier: string; +}) { + const { authBaseUrl, code, redirectUri, verifier } = options; + return await httpJson<{ + access_token: string; + token_type: string; + expires_in: number; + id_token?: string; + refresh_token?: string; + }>(`${authBaseUrl}/v1/token`, { + method: 'POST', + body: { + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + code_verifier: verifier, + }, + signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS), + }); +} + +async function persistInstance(options: { + instanceName: string; + backendBaseUrl: string; + clientId: string; + token: { access_token: string; refresh_token?: string; expires_in: number }; +}) { + const { instanceName, backendBaseUrl, clientId, token } = options; + const secretStore = await getSecretStore(); + await withMetadataLock(async () => { + const service = `backstage-cli:auth-instance:${instanceName}`; + await secretStore.set(service, 'accessToken', token.access_token); + if (token.refresh_token) { + await secretStore.set(service, 'refreshToken', token.refresh_token); + } else { + process.stderr.write( + 'Warning: No refresh token received. You will need to re-authenticate when the access token expires.\n', + ); + } + let existing: StoredInstance | undefined; + try { + existing = await getInstanceByName(instanceName); + } catch { + // new instance + } + await upsertInstance({ + name: instanceName, + baseUrl: backendBaseUrl, + clientId, + issuedAt: Date.now(), + accessTokenExpiresAt: Date.now() + token.expires_in * 1000, + selected: existing?.selected, + }); + }); +} + +function cryptoRandom(): string { + return crypto.randomBytes(32).toString('hex'); +} + +// The react-dev-utils/openBrowser breaks the login URL by encoding the URL parameters again +export function openInBrowser(url: string): void { + const handleError = (error: unknown) => { + const message = error instanceof Error ? error.message : 'Unknown error'; + process.stderr.write( + `Warning: Failed to open browser automatically: ${message}\n`, + ); + process.stderr.write(`Please open this URL manually: ${url}\n`); + }; + + const spawnOpts = { detached: true, stdio: 'ignore' } as const; + let child; + try { + if (process.platform === 'darwin') { + child = spawn('open', [url], spawnOpts); + } else if (process.platform === 'win32') { + child = spawn( + 'powershell', + ['-Command', `Start-Process '${url.replace(/'/g, "''")}'`], + spawnOpts, + ); + } else { + child = spawn('xdg-open', [url], spawnOpts); + } + child.unref(); + child.on('error', handleError); + } catch (error) { + handleError(error); + } +} diff --git a/packages/cli-module-auth/src/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts new file mode 100644 index 0000000000..f79ed2ef35 --- /dev/null +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -0,0 +1,77 @@ +/* + * 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 { cli } from 'cleye'; +import type { CliCommandContext } from '@backstage/cli-node'; +import { getSecretStore } from '../lib/secretStore'; +import { + removeInstance, + withMetadataLock, + getInstanceByName, +} from '../lib/storage'; +import { httpJson } from '../lib/http'; +import { pickInstance } from '../lib/prompt'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { instance: instanceFlag }, + } = cli( + { + help: info, + flags: { + instance: { + type: String, + description: 'Name of the instance to log out', + }, + }, + }, + undefined, + args, + ); + + const { name: instanceName } = await pickInstance(instanceFlag); + + await withMetadataLock(async () => { + const instance = await getInstanceByName(instanceName); + const secretStore = await getSecretStore(); + const service = `backstage-cli:auth-instance:${instanceName}`; + const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; + + if (refreshToken) { + try { + const authBaseUrl = new URL('/api/auth', instance.baseUrl) + .toString() + .replace(/\/$/, ''); + await httpJson(`${authBaseUrl}/v1/revoke`, { + method: 'POST', + body: { + token: refreshToken, + token_type_hint: 'refresh_token', + }, + signal: AbortSignal.timeout(30_000), + }); + } catch { + // ignore errors per RFC 7009 + } + } + + await secretStore.delete(service, 'accessToken'); + await secretStore.delete(service, 'refreshToken'); + await removeInstance(instance.name); + }); + + process.stdout.write('Logged out\n'); +}; diff --git a/packages/cli-module-auth/src/commands/printToken.ts b/packages/cli-module-auth/src/commands/printToken.ts new file mode 100644 index 0000000000..39a78c1832 --- /dev/null +++ b/packages/cli-module-auth/src/commands/printToken.ts @@ -0,0 +1,54 @@ +/* + * 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 { 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'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { instance: instanceFlag }, + } = cli( + { + help: info, + flags: { + instance: { + type: String, + description: 'Name of the instance to use', + }, + }, + }, + undefined, + 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.'); + } + + process.stdout.write(`${accessToken}\n`); +}; diff --git a/packages/cli-module-auth/src/commands/select.ts b/packages/cli-module-auth/src/commands/select.ts new file mode 100644 index 0000000000..ebce23b1cb --- /dev/null +++ b/packages/cli-module-auth/src/commands/select.ts @@ -0,0 +1,43 @@ +/* + * 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 { cli } from 'cleye'; +import type { CliCommandContext } from '@backstage/cli-node'; +import { setSelectedInstance } from '../lib/storage'; +import { pickInstance } from '../lib/prompt'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { instance: instanceFlag }, + } = cli( + { + help: info, + flags: { + instance: { + type: String, + description: 'Name of the instance to select', + }, + }, + }, + undefined, + args, + ); + + const instance = await pickInstance(instanceFlag); + + await setSelectedInstance(instance.name); + process.stderr.write(`Selected instance '${instance.name}'\n`); +}; diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts new file mode 100644 index 0000000000..e1b7c62f72 --- /dev/null +++ b/packages/cli-module-auth/src/commands/show.ts @@ -0,0 +1,72 @@ +/* + * 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 { 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'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { instance: instanceFlag }, + } = cli( + { + help: info, + flags: { + instance: { + type: String, + description: 'Name of the instance to show', + }, + }, + }, + undefined, + args, + ); + + let instance = await getSelectedInstance(instanceFlag); + + if (accessTokenNeedsRefresh(instance)) { + process.stdout.write('Refreshing access token...\n'); + instance = await refreshAccessToken(instance.name); + } + const authBase = new URL('/api/auth', instance.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`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(30_000), + }, + ); + + process.stdout.write(`User: ${userinfo.claims.sub}\n`); + process.stdout.write(`\n`); + process.stdout.write(`Ownership:\n`); + for (const ent of userinfo.claims.ent ?? []) { + process.stdout.write(` - ${ent}\n`); + } +}; diff --git a/packages/cli-module-auth/src/index.ts b/packages/cli-module-auth/src/index.ts new file mode 100644 index 0000000000..fef74ec8a5 --- /dev/null +++ b/packages/cli-module-auth/src/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['auth', 'login'], + description: 'Log in the CLI to a Backstage instance', + execute: { loader: () => import('./commands/login') }, + }); + reg.addCommand({ + path: ['auth', 'logout'], + description: 'Log out the CLI and clear stored credentials', + execute: { loader: () => import('./commands/logout') }, + }); + reg.addCommand({ + path: ['auth', 'show'], + description: 'Show details of an authenticated instance', + execute: { loader: () => import('./commands/show') }, + }); + reg.addCommand({ + path: ['auth', 'list'], + description: 'List authenticated instances', + execute: { loader: () => import('./commands/list') }, + }); + reg.addCommand({ + path: ['auth', 'print-token'], + description: 'Print an access token to stdout (auto-refresh if needed)', + execute: { loader: () => import('./commands/printToken') }, + }); + reg.addCommand({ + path: ['auth', 'select'], + description: 'Select the default instance', + execute: { loader: () => import('./commands/select') }, + }); + }, +}); diff --git a/packages/cli-module-auth/src/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts new file mode 100644 index 0000000000..5ba4ae0295 --- /dev/null +++ b/packages/cli-module-auth/src/lib/auth.test.ts @@ -0,0 +1,336 @@ +/* + * 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 { accessTokenNeedsRefresh, refreshAccessToken } from './auth'; +import * as storage from './storage'; +import * as secretStore from './secretStore'; +import * as http from './http'; + +jest.mock('./storage'); +jest.mock('./secretStore'); +jest.mock('./http'); + +const mockStorage = storage as jest.Mocked; +const mockSecretStore = secretStore as jest.Mocked; +const mockHttp = http as jest.Mocked; + +describe('auth', () => { + describe('accessTokenNeedsRefresh', () => { + it('should return true if token expires within 2 minutes', () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client', + issuedAt: now, + + accessTokenExpiresAt: now + 60_000, // 1 minute from now + }; + + expect(accessTokenNeedsRefresh(instance)).toBe(true); + }); + + it('should return true if token has already expired', () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, // expired 1 minute ago + }; + + expect(accessTokenNeedsRefresh(instance)).toBe(true); + }); + + it('should return false if token is valid for more than 2 minutes', () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client', + issuedAt: now, + + accessTokenExpiresAt: now + 5 * 60_000, // 5 minutes from now + }; + + expect(accessTokenNeedsRefresh(instance)).toBe(false); + }); + + it('should return true at exactly 2 minutes before expiration', () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client', + issuedAt: now, + + accessTokenExpiresAt: now + 2 * 60_000, // exactly 2 minutes from now + }; + + expect(accessTokenNeedsRefresh(instance)).toBe(true); + }); + }); + + describe('refreshAccessToken', () => { + const mockSecretStoreInstance = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockSecretStore.getSecretStore.mockResolvedValue(mockSecretStoreInstance); + }); + + it('should successfully refresh access token', async () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client-id', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, + }; + + mockStorage.withMetadataLock.mockImplementation( + async (fn: () => Promise) => fn(), + ); + mockStorage.getInstanceByName.mockResolvedValue(instance); + mockSecretStoreInstance.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'clientSecret') return 'test-secret'; + if (account === 'refreshToken') return 'old-refresh-token'; + return undefined; + }, + ); + + const tokenResponse = { + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'new-refresh-token', + }; + + mockHttp.httpJson.mockResolvedValue(tokenResponse); + mockStorage.upsertInstance.mockResolvedValue(); + + const result = await refreshAccessToken('test'); + + expect(mockStorage.getInstanceByName).toHaveBeenCalledWith('test'); + expect(mockSecretStoreInstance.get).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:test', + 'refreshToken', + ); + expect(mockHttp.httpJson).toHaveBeenCalledWith( + 'http://localhost:7007/api/auth/v1/token', + { + signal: expect.any(AbortSignal), + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: 'old-refresh-token', + }, + }, + ); + expect(mockSecretStoreInstance.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:test', + 'accessToken', + 'new-access-token', + ); + expect(mockSecretStoreInstance.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:test', + 'refreshToken', + 'new-refresh-token', + ); + expect(mockStorage.upsertInstance).toHaveBeenCalled(); + expect(result.accessTokenExpiresAt).toBeGreaterThan(now); + }); + + it('should throw error if refresh token is missing', async () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client-id', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, + }; + + mockStorage.withMetadataLock.mockImplementation( + async (fn: () => Promise) => fn(), + ); + mockStorage.getInstanceByName.mockResolvedValue(instance); + mockSecretStoreInstance.get.mockResolvedValue(undefined); + + await expect(refreshAccessToken('test')).rejects.toThrow( + 'Access token is expired and no refresh token is available', + ); + }); + + it('should use metadata lock during refresh', async () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client-id', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, + }; + + let lockAcquired = false; + mockStorage.withMetadataLock.mockImplementation( + async (fn: () => Promise) => { + lockAcquired = true; + return fn(); + }, + ); + mockStorage.getInstanceByName.mockResolvedValue(instance); + mockSecretStoreInstance.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'clientSecret') return 'test-secret'; + if (account === 'refreshToken') return 'refresh-token'; + return undefined; + }, + ); + + const tokenResponse = { + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'new-refresh-token', + }; + + mockHttp.httpJson.mockResolvedValue(tokenResponse); + mockStorage.upsertInstance.mockResolvedValue(); + + await refreshAccessToken('test'); + + expect(lockAcquired).toBe(true); + expect(mockStorage.withMetadataLock).toHaveBeenCalled(); + }); + + it('should handle HTTP and network errors during refresh', async () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client-id', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, + }; + + const errorCases = [ + new Error('Request failed with 401 Unauthorized'), + new Error('Network error'), + ]; + + for (const error of errorCases) { + mockStorage.withMetadataLock.mockImplementation( + async (fn: () => Promise) => fn(), + ); + mockStorage.getInstanceByName.mockResolvedValue(instance); + mockSecretStoreInstance.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'clientSecret') return 'test-secret'; + if (account === 'refreshToken') return 'refresh-token'; + return undefined; + }, + ); + + mockHttp.httpJson.mockRejectedValue(error); + + await expect(refreshAccessToken('test')).rejects.toThrow(error.message); + } + }); + + it('should validate token response and reject malformed responses', async () => { + const now = Date.now(); + const instance = { + name: 'test', + baseUrl: 'http://localhost:7007', + clientId: 'test-client-id', + issuedAt: now - 3600_000, + + accessTokenExpiresAt: now - 60_000, + }; + + mockStorage.withMetadataLock.mockImplementation( + async (fn: () => Promise) => fn(), + ); + mockStorage.getInstanceByName.mockResolvedValue(instance); + mockSecretStoreInstance.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'clientSecret') return 'test-secret'; + if (account === 'refreshToken') return 'refresh-token'; + return undefined; + }, + ); + + // Test missing access_token + mockHttp.httpJson.mockResolvedValue({ + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'new-refresh-token', + } as any); + + await expect(refreshAccessToken('test')).rejects.toThrow( + 'Invalid token response', + ); + await expect(refreshAccessToken('test')).rejects.toThrow('access_token'); + + // Test missing expires_in + mockHttp.httpJson.mockResolvedValue({ + access_token: 'new-access-token', + token_type: 'Bearer', + refresh_token: 'new-refresh-token', + } as any); + + await expect(refreshAccessToken('test')).rejects.toThrow( + 'Invalid token response', + ); + await expect(refreshAccessToken('test')).rejects.toThrow('expires_in'); + + // Test missing refresh_token still succeeds and preserves existing token + mockHttp.httpJson.mockResolvedValue({ + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + } as any); + + await expect(refreshAccessToken('test')).resolves.toBeDefined(); + + // Test invalid expires_in (non-positive) + mockHttp.httpJson.mockResolvedValue({ + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 0, + refresh_token: 'new-refresh-token', + } as any); + + await expect(refreshAccessToken('test')).rejects.toThrow( + 'Invalid token response', + ); + await expect(refreshAccessToken('test')).rejects.toThrow('expires_in'); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts new file mode 100644 index 0000000000..3ea7baef5c --- /dev/null +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import { + StoredInstance, + upsertInstance, + withMetadataLock, + getInstanceByName, +} from './storage'; +import { getSecretStore } from './secretStore'; +import { httpJson } from './http'; + +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(), +}); + +export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { + return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; // 2 minutes before expiration +} + +export async function refreshAccessToken( + instanceName: string, +): Promise { + const secretStore = await getSecretStore(); + + return withMetadataLock(async () => { + const instance = await getInstanceByName(instanceName); + + const service = `backstage-cli:auth-instance:${instanceName}`; + const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; + if (!refreshToken) { + throw new Error( + 'Access token is expired and no refresh token is available', + ); + } + + const response = await httpJson( + `${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 secretStore.set(service, 'accessToken', token.access_token); + if (token.refresh_token) { + await secretStore.set(service, 'refreshToken', token.refresh_token); + } + const newInstance = { + ...instance, + issuedAt: Date.now(), + accessTokenExpiresAt: Date.now() + token.expires_in * 1000, + }; + await upsertInstance(newInstance); + return newInstance; + }); +} diff --git a/packages/cli-module-auth/src/lib/http.test.ts b/packages/cli-module-auth/src/lib/http.test.ts new file mode 100644 index 0000000000..74416a0dee --- /dev/null +++ b/packages/cli-module-auth/src/lib/http.test.ts @@ -0,0 +1,269 @@ +/* + * 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 fetch from 'cross-fetch'; +import { httpJson } from './http'; + +jest.mock('cross-fetch'); + +const mockFetch = fetch as jest.MockedFunction; + +describe('http', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('httpJson', () => { + it('should make successful GET request and parse JSON', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ data: 'test' }), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + const result = await httpJson('https://example.com/api'); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + body: undefined, + }), + ); + expect(result).toEqual({ data: 'test' }); + }); + + it('should make POST request with JSON body', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true }), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + const body = { username: 'test', password: 'secret' }; + const result = await httpJson('https://example.com/api', { + method: 'POST', + body, + }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }), + ); + expect(result).toEqual({ success: true }); + }); + + it('should include and merge custom headers', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ data: 'test' }), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + // Test custom headers without body + await httpJson('https://example.com/api', { + headers: { + Authorization: 'Bearer token', + 'X-Custom': 'value', + }, + }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + headers: { + Authorization: 'Bearer token', + 'X-Custom': 'value', + }, + }), + ); + + // Test merging headers with content-type when body is present + await httpJson('https://example.com/api', { + method: 'POST', + body: { data: 'test' }, + headers: { + Authorization: 'Bearer token', + }, + }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + }), + ); + }); + + it('should throw ResponseError for non-ok responses', async () => { + const errorCases = [ + { status: 404, statusText: 'Not Found' }, + { status: 401, statusText: 'Unauthorized' }, + { status: 500, statusText: 'Internal Server Error' }, + ]; + + for (const { status, statusText } of errorCases) { + const mockResponse = { + ok: false, + status, + statusText, + url: 'https://example.com/api', + text: jest.fn().mockResolvedValue('Error'), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + await expect(httpJson('https://example.com/api')).rejects.toThrow( + `Request failed with ${status} ${statusText}`, + ); + } + }); + + it('should pass through abort signal from caller', async () => { + const abortController = new AbortController(); + let rejectFn: (error: Error) => void; + const mockResponse = new Promise((_, reject) => { + rejectFn = reject; + setTimeout(() => { + reject(new Error('Request should have been aborted')); + }, 60000); // 60 seconds + }); + + mockFetch.mockImplementation((_url, options) => { + const signal = options?.signal as AbortSignal; + signal?.addEventListener('abort', () => { + rejectFn(new Error('The operation was aborted')); + }); + return mockResponse as any; + }); + + const requestPromise = httpJson('https://example.com/api', { + signal: abortController.signal, + }); + + // Abort the request + abortController.abort(); + + await expect(requestPromise).rejects.toThrow('The operation was aborted'); + }); + + it('should handle JSON parsing errors gracefully', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + await expect(httpJson('https://example.com/api')).rejects.toThrow( + 'Invalid JSON', + ); + }); + + it('should support different HTTP methods', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true }), + }; + + for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) { + mockFetch.mockResolvedValue(mockResponse as any); + + await httpJson('https://example.com/api', { method }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + method, + }), + ); + } + }); + + it('should handle various response body types', async () => { + const testCases = [ + { body: null, expected: null }, + { body: [1, 2, 3], expected: [1, 2, 3] }, + { body: { data: 'test' }, expected: { data: 'test' } }, + ]; + + for (const { body, expected } of testCases) { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue(body), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + const result = await httpJson('https://example.com/api'); + expect(result).toEqual(expected); + } + }); + + it('should handle network errors', async () => { + const networkError = new Error('Network error'); + mockFetch.mockRejectedValue(networkError); + + await expect(httpJson('https://example.com/api')).rejects.toThrow( + 'Network error', + ); + }); + + it('should use custom abort signal if provided', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ data: 'test' }), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + const customController = new AbortController(); + await httpJson('https://example.com/api', { + signal: customController.signal, + }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ + signal: expect.any(AbortSignal), + }), + ); + }); + + it('should handle malformed URLs gracefully', async () => { + const networkError = new TypeError('Failed to parse URL'); + mockFetch.mockRejectedValue(networkError); + + await expect(httpJson('not-a-valid-url')).rejects.toThrow(); + }); + + it('should handle very large response bodies', async () => { + const largeData = { items: Array(10000).fill({ data: 'x'.repeat(100) }) }; + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue(largeData), + }; + mockFetch.mockResolvedValue(mockResponse as any); + + const result = await httpJson('https://example.com/api'); + expect(result).toEqual(largeData); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts new file mode 100644 index 0000000000..307d7d2334 --- /dev/null +++ b/packages/cli-module-auth/src/lib/http.ts @@ -0,0 +1,40 @@ +/* + * 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 fetch from 'cross-fetch'; +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/src/lib/localServer.test.ts b/packages/cli-module-auth/src/lib/localServer.test.ts new file mode 100644 index 0000000000..3b03331d3e --- /dev/null +++ b/packages/cli-module-auth/src/lib/localServer.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { startCallbackServer } from './localServer'; + +describe('localServer', () => { + it('should start on port 8055, handle requests, and resolve the code', async () => { + const { url, waitForCode, close } = await startCallbackServer({ + state: 'test-state', + }); + + expect(url).toBe('http://127.0.0.1:8055/callback'); + + // 404 for non-callback paths + const notFoundResponse = await fetch( + url.replace('/callback', '/other-path'), + ); + expect(notFoundResponse.status).toBe(404); + + // 400 for missing code + const missingCodeResponse = await fetch(`${url}?state=test-state`); + expect(missingCodeResponse.status).toBe(400); + expect(await missingCodeResponse.text()).toBe('Missing code'); + + // 400 for mismatched state + const mismatchResponse = await fetch( + `${url}?code=test-code&state=wrong-state`, + ); + expect(mismatchResponse.status).toBe(400); + expect(await mismatchResponse.text()).toBe('State mismatch'); + + // 200 for valid callback with matching state + const codePromise = waitForCode(); + const specialCode = 'test-code+with/special=chars'; + const successResponse = await fetch( + `${url}?code=${encodeURIComponent( + specialCode, + )}&state=${encodeURIComponent('test-state')}`, + ); + expect(successResponse.status).toBe(200); + expect(await successResponse.text()).toBe('You may now close this window.'); + expect(successResponse.headers.get('content-type')).toBe( + 'text/plain; charset=utf-8', + ); + + const result = await codePromise; + expect(result.code).toBe(specialCode); + expect(result.state).toBe('test-state'); + + await close(); + }); +}); diff --git a/packages/cli-module-auth/src/lib/localServer.ts b/packages/cli-module-auth/src/lib/localServer.ts new file mode 100644 index 0000000000..0b71075a5b --- /dev/null +++ b/packages/cli-module-auth/src/lib/localServer.ts @@ -0,0 +1,98 @@ +/* + * 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 http from 'node:http'; +import { URL } from 'node:url'; + +const CALLBACK_PORT = 8055; + +export async function startCallbackServer(options: { state: string }): Promise<{ + url: string; + waitForCode: () => Promise<{ code: string; state?: string }>; + close: () => Promise; +}> { + const server = http.createServer(); + + let resolveResult: + | ((v: { code: string; state?: string }) => void) + | undefined; + const resultPromise = new Promise<{ code: string; state?: string }>( + resolve => { + resolveResult = resolve; + }, + ); + + server.on('request', (req, res) => { + if (!req.url) { + res.statusCode = 400; + res.end('Bad Request'); + return; + } + const u = new URL(req.url, 'http://127.0.0.1'); + if (u.pathname !== '/callback') { + res.statusCode = 404; + res.end('Not Found'); + return; + } + const code = u.searchParams.get('code') ?? undefined; + const state = u.searchParams.get('state') ?? undefined; + if (!code) { + res.statusCode = 400; + res.end('Missing code'); + return; + } + if (state !== options.state) { + res.statusCode = 400; + res.end('State mismatch'); + return; + } + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.end('You may now close this window.'); + resolveResult?.({ code, state }); + }); + + const port = await new Promise((resolve, reject) => { + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + reject( + new Error( + `Port ${CALLBACK_PORT} is already in use. Close the application using it and try again.`, + ), + ); + } else { + reject(err); + } + }); + server.listen(CALLBACK_PORT, '127.0.0.1', () => { + const address = server.address(); + if (typeof address === 'object' && address && 'port' in address) { + resolve(address.port); + } else { + reject(new Error('Failed to bind local server')); + } + }); + }); + + return { + url: `http://127.0.0.1:${port}/callback`, + waitForCode: () => resultPromise, + close: async () => { + server.closeAllConnections(); + return new Promise(resolve => server.close(() => resolve())); + }, + }; +} diff --git a/packages/cli-module-auth/src/lib/pkce.test.ts b/packages/cli-module-auth/src/lib/pkce.test.ts new file mode 100644 index 0000000000..09db60745a --- /dev/null +++ b/packages/cli-module-auth/src/lib/pkce.test.ts @@ -0,0 +1,178 @@ +/* + * 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 crypto from 'node:crypto'; +import { generateVerifier, challengeFromVerifier } from './pkce'; + +describe('pkce', () => { + describe('generateVerifier', () => { + it('should generate verifiers with proper encoding and length', () => { + // Test default length + const defaultVerifier = generateVerifier(); + expect(defaultVerifier).toBeDefined(); + expect(typeof defaultVerifier).toBe('string'); + expect(defaultVerifier.length).toBeGreaterThan(0); + + // Test custom lengths + const shortVerifier = generateVerifier(32); + const longVerifier = generateVerifier(96); + expect(shortVerifier).toBeDefined(); + expect(longVerifier).toBeDefined(); + expect(shortVerifier.length).toBeGreaterThan(0); + expect(longVerifier.length).toBeGreaterThan(shortVerifier.length); + + // Test base64url encoding (no padding, proper characters) + const verifier = generateVerifier(); + expect(verifier).not.toContain('='); + expect(verifier).not.toContain('+'); + expect(verifier).not.toContain('/'); + expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/); + + // Test uniqueness + const verifier1 = generateVerifier(); + const verifier2 = generateVerifier(); + expect(verifier1).not.toBe(verifier2); + }); + + it('should enforce minimum and maximum length constraints', () => { + // Test minimum length enforcement + const minVerifier = generateVerifier(10); // Less than minimum + expect(minVerifier).toBeDefined(); + expect(minVerifier.length).toBeGreaterThanOrEqual(43); // 32 bytes = 43 base64url chars + + // Test maximum length enforcement + const maxVerifier = generateVerifier(200); // More than maximum + expect(maxVerifier).toBeDefined(); + expect(maxVerifier.length).toBeLessThanOrEqual(128); // 96 bytes = 128 base64url chars + }); + + it('should produce consistent results for same byte sequence', () => { + // Mock crypto.randomBytes to return predictable values + const mockBytes = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + jest.spyOn(crypto, 'randomBytes').mockImplementation(_count => mockBytes); + + const verifier1 = generateVerifier(8); + const verifier2 = generateVerifier(8); + + expect(verifier1).toBe(verifier2); + + (crypto.randomBytes as jest.Mock).mockRestore(); + }); + }); + + describe('challengeFromVerifier', () => { + it('should generate challenges with proper encoding and consistency', () => { + const verifier = 'test-verifier-string'; + const challenge = challengeFromVerifier(verifier); + + // Basic properties + expect(challenge).toBeDefined(); + expect(typeof challenge).toBe('string'); + expect(challenge.length).toBe(43); // SHA-256 = 32 bytes = 43 base64url chars + + // Base64url encoding (no padding, proper characters) + expect(challenge).not.toContain('='); + expect(challenge).not.toContain('+'); + expect(challenge).not.toContain('/'); + expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/); + + // Consistency for same verifier + const challenge1 = challengeFromVerifier(verifier); + const challenge2 = challengeFromVerifier(verifier); + expect(challenge1).toBe(challenge2); + expect(challenge1).toBe(challenge); + + // Different challenges for different verifiers + const verifier2 = 'test-verifier-2'; + const challenge3 = challengeFromVerifier(verifier2); + expect(challenge3).not.toBe(challenge); + }); + + it('should handle edge cases for verifier length', () => { + // Empty verifier + const emptyChallenge = challengeFromVerifier(''); + expect(emptyChallenge).toBeDefined(); + expect(emptyChallenge.length).toBe(43); + + // Very long verifier + const longVerifier = 'a'.repeat(1000); + const longChallenge = challengeFromVerifier(longVerifier); + expect(longChallenge).toBeDefined(); + expect(longChallenge.length).toBe(43); // SHA-256 always produces 32 bytes + }); + + it('should produce RFC 7636 compliant challenge', () => { + // Test with a known verifier + const verifier = generateVerifier(); + const challenge = challengeFromVerifier(verifier); + + // Verify it's using SHA-256 correctly + const expectedHash = crypto + .createHash('sha256') + .update(verifier) + .digest(); + const expectedChallenge = expectedHash + .toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + expect(challenge).toBe(expectedChallenge); + }); + }); + + describe('PKCE flow integration', () => { + it('should generate valid verifier and challenge pair', () => { + const verifier = generateVerifier(); + const challenge = challengeFromVerifier(verifier); + + expect(verifier).toBeDefined(); + expect(challenge).toBeDefined(); + expect(verifier).not.toBe(challenge); + + // Verifier should be longer than challenge + expect(verifier.length).toBeGreaterThan(challenge.length); + }); + + it('should generate multiple unique pairs', () => { + const pair1 = { + verifier: generateVerifier(), + challenge: '', + }; + pair1.challenge = challengeFromVerifier(pair1.verifier); + + const pair2 = { + verifier: generateVerifier(), + challenge: '', + }; + pair2.challenge = challengeFromVerifier(pair2.verifier); + + expect(pair1.verifier).not.toBe(pair2.verifier); + expect(pair1.challenge).not.toBe(pair2.challenge); + }); + + it('should maintain one-to-one mapping between verifier and challenge', () => { + const verifier = generateVerifier(); + const challenge1 = challengeFromVerifier(verifier); + const challenge2 = challengeFromVerifier(verifier); + const challenge3 = challengeFromVerifier(verifier); + + // All challenges from same verifier should be identical + expect(challenge1).toBe(challenge2); + expect(challenge2).toBe(challenge3); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/pkce.ts b/packages/cli-module-auth/src/lib/pkce.ts new file mode 100644 index 0000000000..fd5bba2d70 --- /dev/null +++ b/packages/cli-module-auth/src/lib/pkce.ts @@ -0,0 +1,36 @@ +/* + * 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 crypto from 'node:crypto'; + +function base64url(input: Buffer): string { + return input + .toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +export function generateVerifier(length = 64): string { + // length in bytes ~ 48 results in 64 base64url chars; keep within 43..128 chars + const bytes = crypto.randomBytes(Math.max(32, Math.min(96, length))); + return base64url(bytes); +} + +export function challengeFromVerifier(verifier: string): string { + const hash = crypto.createHash('sha256').update(verifier).digest(); + return base64url(hash); +} diff --git a/packages/cli-module-auth/src/lib/prompt.test.ts b/packages/cli-module-auth/src/lib/prompt.test.ts new file mode 100644 index 0000000000..acb28fc124 --- /dev/null +++ b/packages/cli-module-auth/src/lib/prompt.test.ts @@ -0,0 +1,191 @@ +/* + * 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 inquirer from 'inquirer'; +import { pickInstance } from './prompt'; +import * as storage from './storage'; + +jest.mock('inquirer'); +jest.mock('./storage'); + +const mockStorage = storage as jest.Mocked; +const mockInquirer = inquirer as jest.Mocked; + +describe('prompt', () => { + describe('pickInstance', () => { + const mockInstances = [ + { + name: 'production', + baseUrl: 'https://backstage.example.com', + clientId: 'prod-client', + issuedAt: Date.now(), + accessToken: 'prod-token', + accessTokenExpiresAt: Date.now() + 3600_000, + selected: true, + }, + { + name: 'staging', + baseUrl: 'https://staging.backstage.example.com', + clientId: 'staging-client', + issuedAt: Date.now(), + accessToken: 'staging-token', + accessTokenExpiresAt: Date.now() + 3600_000, + selected: false, + }, + { + name: 'local', + baseUrl: 'http://localhost:7007', + clientId: 'local-client', + issuedAt: Date.now(), + accessToken: 'local-token', + accessTokenExpiresAt: Date.now() + 3600_000, + selected: false, + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return instance by name if provided', async () => { + mockStorage.getInstanceByName.mockResolvedValue(mockInstances[1]); + + const result = await pickInstance('staging'); + + expect(result).toEqual(mockInstances[1]); + expect(mockStorage.getInstanceByName).toHaveBeenCalledWith('staging'); + expect(mockInquirer.prompt).not.toHaveBeenCalled(); + }); + + it('should prompt for instance and show selected instance with asterisk prefix', async () => { + // Test with production selected + mockStorage.getAllInstances.mockResolvedValue({ + instances: mockInstances, + selected: mockInstances[0], + }); + mockInquirer.prompt.mockResolvedValue({ choice: 'staging' }); + + const result = await pickInstance(); + + expect(mockInquirer.prompt).toHaveBeenCalledWith([ + { + type: 'list', + name: 'choice', + message: 'Select instance:', + choices: [ + { + name: '* production (https://backstage.example.com)', + value: 'production', + }, + { + name: ' staging (https://staging.backstage.example.com)', + value: 'staging', + }, + { + name: ' local (http://localhost:7007)', + value: 'local', + }, + ], + default: 'production', + }, + ]); + expect(result).toEqual(mockInstances[1]); + + // Test with staging selected + mockStorage.getAllInstances.mockResolvedValue({ + instances: mockInstances, + selected: mockInstances[1], + }); + mockInquirer.prompt.mockResolvedValue({ choice: 'staging' }); + + await pickInstance(); + + expect(mockInquirer.prompt).toHaveBeenCalledWith([ + expect.objectContaining({ + choices: [ + { + name: ' production (https://backstage.example.com)', + value: 'production', + }, + { + name: '* staging (https://staging.backstage.example.com)', + value: 'staging', + }, + { + name: ' local (http://localhost:7007)', + value: 'local', + }, + ], + default: 'staging', + }), + ]); + }); + + it('should throw error if no instances are available', async () => { + mockStorage.getAllInstances.mockResolvedValue({ + instances: [], + selected: undefined, + }); + + await expect(pickInstance()).rejects.toThrow( + 'No instances found. Run "auth login" to authenticate first.', + ); + }); + + it('should throw error if selected instance is not found', async () => { + mockStorage.getAllInstances.mockResolvedValue({ + instances: mockInstances, + selected: mockInstances[0], + }); + mockInquirer.prompt.mockResolvedValue({ choice: 'non-existent' }); + + await expect(pickInstance()).rejects.toThrow( + "Instance 'non-existent' not found", + ); + }); + + it('should handle single instance and use selected instance as default', async () => { + // Test single instance + const singleInstance = [mockInstances[0]]; + mockStorage.getAllInstances.mockResolvedValue({ + instances: singleInstance, + selected: mockInstances[0], + }); + mockInquirer.prompt.mockResolvedValue({ choice: 'production' }); + + const result = await pickInstance(); + + expect(result).toEqual(mockInstances[0]); + expect(mockInquirer.prompt).toHaveBeenCalled(); + + // Test default selection matches selected instance + const selectedInstance = mockInstances[2]; + mockStorage.getAllInstances.mockResolvedValue({ + instances: mockInstances, + selected: selectedInstance, + }); + mockInquirer.prompt.mockResolvedValue({ choice: 'local' }); + + await pickInstance(); + + expect(mockInquirer.prompt).toHaveBeenCalledWith([ + expect.objectContaining({ + default: 'local', + }), + ]); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/prompt.ts b/packages/cli-module-auth/src/lib/prompt.ts new file mode 100644 index 0000000000..46a7430548 --- /dev/null +++ b/packages/cli-module-auth/src/lib/prompt.ts @@ -0,0 +1,58 @@ +/* + * 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 inquirer from 'inquirer'; +import { getInstanceByName, getAllInstances, StoredInstance } from './storage'; + +export async function pickInstance(name?: string): Promise { + if (name) { + return getInstanceByName(name); + } + + const { instances, selected } = await getAllInstances(); + if (instances.length === 0) { + throw new Error( + 'No instances found. Run "auth login" to authenticate first.', + ); + } + return await promptForInstance(instances, selected); +} + +async function promptForInstance( + instances: StoredInstance[], + selected: StoredInstance | undefined, +): Promise { + const choices = instances.map(i => ({ + name: `${i.name === selected?.name ? '* ' : ' '}${i.name} (${i.baseUrl})`, + value: i.name, + })); + + const { choice } = await inquirer.prompt<{ choice: string }>([ + { + type: 'list', + name: 'choice', + message: 'Select instance:', + choices, + default: selected?.name, + }, + ]); + + const instance = instances.find(i => i.name === choice); + if (!instance) { + throw new Error(`Instance '${choice}' not found`); + } + return instance; +} diff --git a/packages/cli-module-auth/src/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts new file mode 100644 index 0000000000..6729f7c6a5 --- /dev/null +++ b/packages/cli-module-auth/src/lib/secretStore.test.ts @@ -0,0 +1,250 @@ +/* + * 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. + */ + +jest.mock('keytar', () => { + throw new Error('keytar not available'); +}); + +import fs from 'fs-extra'; +import path from 'node:path'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { getSecretStore, resetSecretStore } from './secretStore'; + +const mockDir = createMockDirectory(); + +describe('secretStore', () => { + beforeEach(() => { + mockDir.clear(); + process.env.XDG_DATA_HOME = mockDir.resolve('data'); + resetSecretStore(); + }); + + afterEach(() => { + delete process.env.XDG_DATA_HOME; + resetSecretStore(); + }); + + describe('FileSecretStore', () => { + it('should store and retrieve secrets', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + const result = await store.get('test-service', 'test-account'); + + expect(result).toBe('test-secret'); + }); + + it('should return undefined for non-existent secrets', async () => { + const store = await getSecretStore(); + const result = await store.get('test-service', 'test-account'); + + expect(result).toBeUndefined(); + }); + + it('should delete secrets', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + + let result = await store.get('test-service', 'test-account'); + expect(result).toBe('test-secret'); + + await store.delete('test-service', 'test-account'); + + result = await store.get('test-service', 'test-account'); + expect(result).toBeUndefined(); + }); + + it('should not throw when deleting non-existent secrets', async () => { + const store = await getSecretStore(); + + await expect( + store.delete('non-existent-service', 'non-existent-account'), + ).resolves.not.toThrow(); + }); + + it('should create files with correct directory structure', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + + const expectedDir = path.join( + mockDir.resolve('data'), + 'backstage-cli', + 'auth-secrets', + encodeURIComponent('test-service'), + ); + const expectedFile = path.join( + expectedDir, + `${encodeURIComponent('test-account')}.secret`, + ); + + expect(await fs.pathExists(expectedFile)).toBe(true); + expect(await fs.pathExists(expectedDir)).toBe(true); + }); + + it('should create files with correct permissions (0o600)', async () => { + // File permissions are not reliably enforced on Windows + if (process.platform === 'win32') { + return; + } + + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + + const expectedFile = path.join( + mockDir.resolve('data'), + 'backstage-cli', + 'auth-secrets', + encodeURIComponent('test-service'), + `${encodeURIComponent('test-account')}.secret`, + ); + + const stats = await fs.stat(expectedFile); + const mode = stats.mode & 0o777; + expect(mode).toBe(0o600); + }); + + it('should encode service and account names in file path', async () => { + const store = await getSecretStore(); + await store.set('my-service/test', 'my-account@test', 'test-secret'); + + const result = await store.get('my-service/test', 'my-account@test'); + expect(result).toBe('test-secret'); + + const expectedFile = path.join( + mockDir.resolve('data'), + 'backstage-cli', + 'auth-secrets', + encodeURIComponent('my-service/test'), + `${encodeURIComponent('my-account@test')}.secret`, + ); + expect(await fs.pathExists(expectedFile)).toBe(true); + }); + + it('should handle unicode characters in service and account names', async () => { + const store = await getSecretStore(); + await store.set('service-测试', 'account-🚀', 'test-secret'); + + const result = await store.get('service-测试', 'account-🚀'); + expect(result).toBe('test-secret'); + }); + + it('should handle multiple secrets for same service', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'account1', 'secret1'); + await store.set('test-service', 'account2', 'secret2'); + + const result1 = await store.get('test-service', 'account1'); + const result2 = await store.get('test-service', 'account2'); + + expect(result1).toBe('secret1'); + expect(result2).toBe('secret2'); + }); + + it('should handle multiple services', async () => { + const store = await getSecretStore(); + await store.set('service1', 'account', 'secret1'); + await store.set('service2', 'account', 'secret2'); + + const result1 = await store.get('service1', 'account'); + const result2 = await store.get('service2', 'account'); + + expect(result1).toBe('secret1'); + expect(result2).toBe('secret2'); + }); + + it('should update existing secrets', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'old-secret'); + await store.set('test-service', 'test-account', 'new-secret'); + + const result = await store.get('test-service', 'test-account'); + expect(result).toBe('new-secret'); + }); + + it('should handle empty string secrets', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', ''); + + const result = await store.get('test-service', 'test-account'); + expect(result).toBe(''); + }); + + it('should handle very long secrets', async () => { + const store = await getSecretStore(); + const longSecret = 'a'.repeat(10000); + await store.set('test-service', 'test-account', longSecret); + + const result = await store.get('test-service', 'test-account'); + expect(result).toBe(longSecret); + }); + + it('should use XDG_DATA_HOME when set', async () => { + const customDataHome = mockDir.resolve('custom-data'); + process.env.XDG_DATA_HOME = customDataHome; + resetSecretStore(); + + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + + const expectedFile = path.join( + customDataHome, + 'backstage-cli', + 'auth-secrets', + encodeURIComponent('test-service'), + `${encodeURIComponent('test-account')}.secret`, + ); + expect(await fs.pathExists(expectedFile)).toBe(true); + + const result = await store.get('test-service', 'test-account'); + expect(result).toBe('test-secret'); + }); + }); + + describe('getSecretStore singleton', () => { + it('should return the same instance on multiple calls', async () => { + const store1 = await getSecretStore(); + const store2 = await getSecretStore(); + + expect(store1).toBe(store2); + }); + + it('should create new instance after reset', async () => { + const store1 = await getSecretStore(); + resetSecretStore(); + const store2 = await getSecretStore(); + + expect(store1).not.toBe(store2); + }); + }); + + describe('fallback behavior', () => { + it('should fall back to FileSecretStore when keytar is not available', async () => { + const store = await getSecretStore(); + await store.set('test-service', 'test-account', 'test-secret'); + + const result = await store.get('test-service', 'test-account'); + expect(result).toBe('test-secret'); + + const expectedFile = path.join( + mockDir.resolve('data'), + 'backstage-cli', + 'auth-secrets', + encodeURIComponent('test-service'), + `${encodeURIComponent('test-account')}.secret`, + ); + expect(await fs.pathExists(expectedFile)).toBe(true); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-module-auth/src/lib/secretStore.ts new file mode 100644 index 0000000000..55fac878b7 --- /dev/null +++ b/packages/cli-module-auth/src/lib/secretStore.ts @@ -0,0 +1,110 @@ +/* + * 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'; + +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; + +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-module-auth/src/lib/storage.test.ts b/packages/cli-module-auth/src/lib/storage.test.ts new file mode 100644 index 0000000000..07ffba9510 --- /dev/null +++ b/packages/cli-module-auth/src/lib/storage.test.ts @@ -0,0 +1,420 @@ +/* + * 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 path from 'node:path'; +import { NotFoundError } from '@backstage/errors'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + getAllInstances, + getSelectedInstance, + getInstanceByName, + upsertInstance, + removeInstance, + setSelectedInstance, + withMetadataLock, + StoredInstance, +} from './storage'; + +const mockDir = createMockDirectory(); + +describe('storage', () => { + const mockInstance1: StoredInstance = { + name: 'production', + baseUrl: 'https://backstage.example.com', + clientId: 'prod-client', + issuedAt: Date.now(), + accessTokenExpiresAt: Date.now() + 3600_000, + selected: true, + }; + + const mockInstance2: StoredInstance = { + name: 'staging', + baseUrl: 'https://staging.backstage.example.com', + clientId: 'staging-client', + issuedAt: Date.now(), + accessTokenExpiresAt: Date.now() + 3600_000, + selected: false, + }; + + beforeEach(() => { + mockDir.clear(); + process.env.XDG_CONFIG_HOME = mockDir.resolve('config'); + }); + + afterEach(() => { + delete process.env.XDG_CONFIG_HOME; + }); + + describe('getAllInstances', () => { + it('should return empty array if file does not exist or is empty', async () => { + const result1 = await getAllInstances(); + expect(result1).toEqual({ instances: [], selected: undefined }); + + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': '', + }); + + const result2 = await getAllInstances(); + expect(result2).toEqual({ instances: [], selected: undefined }); + }); + + it('should parse and return instances from YAML', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + selected: true + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} +`, + }); + + const result = await getAllInstances(); + + expect(result.instances).toHaveLength(2); + expect(result.selected?.name).toBe('production'); + }); + + it('should select first instance if none marked as selected', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} +`, + }); + + const result = await getAllInstances(); + + expect(result.selected?.name).toBe('production'); + }); + + it('should return empty array if YAML parsing fails', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': 'invalid: yaml: [', + }); + + const result = await getAllInstances(); + + expect(result).toEqual({ instances: [], selected: undefined }); + }); + + it('should normalize selected property across instances', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + selected: true + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} + selected: true +`, + }); + + const result = await getAllInstances(); + + const selectedCount = result.instances.filter(i => i.selected).length; + expect(selectedCount).toBe(1); + }); + }); + + describe('getSelectedInstance', () => { + it('should return instance by name if provided', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} +`, + }); + + const result = await getSelectedInstance('production'); + + expect(result.name).toBe('production'); + }); + + it('should return selected instance if no name provided', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} + selected: true +`, + }); + + const result = await getSelectedInstance(); + + expect(result.name).toBe('staging'); + }); + + it('should throw error if no instances exist', async () => { + await expect(getSelectedInstance()).rejects.toThrow( + 'No instances found. Run "auth login" to authenticate first.', + ); + }); + }); + + describe('getInstanceByName', () => { + it('should return instance with matching name', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} +`, + }); + + const result = await getInstanceByName('production'); + + expect(result.name).toBe('production'); + }); + + it('should throw NotFoundError if instance does not exist', async () => { + await expect(getInstanceByName('nonexistent')).rejects.toThrow( + NotFoundError, + ); + await expect(getInstanceByName('nonexistent')).rejects.toThrow( + "Instance 'nonexistent' not found", + ); + }); + }); + + describe('upsertInstance', () => { + it('should add new instance if it does not exist', async () => { + await upsertInstance(mockInstance1); + + const result = await getAllInstances(); + expect(result.instances).toHaveLength(1); + expect(result.instances[0].name).toBe('production'); + }); + + it('should update existing instance', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} +`, + }); + + const updatedInstance = { + ...mockInstance1, + clientId: 'updated-client', + }; + + await upsertInstance(updatedInstance); + + const result = await getInstanceByName('production'); + expect(result.clientId).toBe('updated-client'); + }); + }); + + describe('removeInstance', () => { + it('should remove instance with matching name', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} +`, + }); + + await removeInstance('production'); + + const result = await getAllInstances(); + expect(result.instances).toHaveLength(1); + expect(result.instances[0].name).toBe('staging'); + }); + + it('should do nothing if instance does not exist', async () => { + await removeInstance('nonexistent'); + + const result = await getAllInstances(); + expect(result.instances).toHaveLength(0); + }); + }); + + describe('setSelectedInstance', () => { + it('should set selected instance and unselect others', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production + baseUrl: https://backstage.example.com + clientId: prod-client + issuedAt: ${mockInstance1.issuedAt} + + accessTokenExpiresAt: ${mockInstance1.accessTokenExpiresAt} + selected: true + - name: staging + baseUrl: https://staging.backstage.example.com + clientId: staging-client + issuedAt: ${mockInstance2.issuedAt} + + accessTokenExpiresAt: ${mockInstance2.accessTokenExpiresAt} +`, + }); + + await setSelectedInstance('staging'); + + const result = await getAllInstances(); + expect(result.selected?.name).toBe('staging'); + + const prodInstance = result.instances.find(i => i.name === 'production'); + expect(prodInstance?.selected).toBe(false); + }); + + it('should throw error if instance does not exist', async () => { + await expect(setSelectedInstance('nonexistent')).rejects.toThrow( + "Unknown instance 'nonexistent'", + ); + }); + }); + + describe('withMetadataLock', () => { + it('should acquire and release lock', async () => { + const callback = jest.fn().mockResolvedValue('result'); + const result = await withMetadataLock(callback); + + expect(callback).toHaveBeenCalled(); + expect(result).toBe('result'); + }); + + it('should release lock even if callback throws', async () => { + const error = new Error('Test error'); + const callback = jest.fn().mockRejectedValue(error); + + await expect(withMetadataLock(callback)).rejects.toThrow(error); + + // Lock should still be released, allowing subsequent calls + const callback2 = jest.fn().mockResolvedValue('result'); + await expect(withMetadataLock(callback2)).resolves.toBe('result'); + }); + }); + + describe('file path resolution', () => { + it('should use XDG_CONFIG_HOME when set', async () => { + const customConfigHome = mockDir.resolve('custom-config'); + process.env.XDG_CONFIG_HOME = customConfigHome; + + await upsertInstance(mockInstance1); + + const result = await getAllInstances(); + expect(result.instances).toHaveLength(1); + expect(result.instances[0].name).toBe('production'); + + // Verify file was created in custom location + const expectedFile = path.join( + customConfigHome, + 'backstage-cli', + 'auth-instances.yaml', + ); + expect(await fs.pathExists(expectedFile)).toBe(true); + }); + + it('should create files with correct permissions (0o600)', async () => { + // File permissions are not reliably enforced on Windows + if (process.platform === 'win32') { + return; + } + + await upsertInstance(mockInstance1); + + const file = path.join( + mockDir.resolve('config'), + 'backstage-cli', + 'auth-instances.yaml', + ); + const stats = await fs.stat(file); + const mode = stats.mode & 0o777; + expect(mode).toBe(0o600); + }); + + it('should handle invalid schema and missing fields gracefully', async () => { + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: "" + baseUrl: not-a-url + clientId: "" +`, + }); + + const result1 = await getAllInstances(); + expect(result1.instances).toHaveLength(0); + + mockDir.setContent({ + 'config/backstage-cli/auth-instances.yaml': `instances: + - name: production +`, + }); + + const result2 = await getAllInstances(); + expect(result2.instances).toHaveLength(0); + }); + }); +}); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts new file mode 100644 index 0000000000..c2eb153112 --- /dev/null +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -0,0 +1,177 @@ +/* + * 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 lockfile from 'proper-lockfile'; +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(), +}); + +export type StoredInstance = z.infer; + +const authYamlSchema = z.object({ + instances: z.array(storedInstanceSchema).default([]), +}); + +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); +} + +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: [] }; + } +} + +async function writeAll(data: { instances: StoredInstance[] }): Promise { + const file = getMetadataFilePath(); + await fs.ensureDir(path.dirname(file)); + const yaml = YAML.stringify(authYamlSchema.parse(data), { indentSeq: false }); + await fs.writeFile(file, yaml, { encoding: 'utf8', mode: 0o600 }); +} + +export async function getAllInstances(): Promise<{ + instances: StoredInstance[]; + selected: StoredInstance | undefined; +}> { + 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, + })), + selected, + }; +} + +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; +} + +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; +} + +export async function upsertInstance(instance: StoredInstance): Promise { + const data = await readAll(); + const idx = data.instances.findIndex(i => i.name === instance.name); + if (idx === -1) { + data.instances.push(instance); + } else { + data.instances[idx] = instance; + } + await writeAll(data); +} + +export async function removeInstance(name: string): Promise { + const data = await readAll(); + const next = data.instances.filter(i => i.name !== name); + if (next.length !== data.instances.length) { + await writeAll({ instances: next }); + } +} + +export async function setSelectedInstance(name: string): Promise { + return withMetadataLock(async () => { + const data = await readAll(); + let found = false; + data.instances = data.instances.map(i => { + if (i.name === name) { + found = true; + return { ...i, selected: true }; + } + const { selected, ...rest } = i; + return { ...rest, selected: false }; + }); + if (!found) { + throw new Error(`Unknown instance '${name}'`); + } + await writeAll(data); + }); +} + +export async function withMetadataLock(fn: () => Promise): Promise { + const file = getMetadataFilePath(); + await fs.ensureDir(path.dirname(file)); + if (!(await fs.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(); + } +} diff --git a/packages/cli-module-build/.eslintrc.js b/packages/cli-module-build/.eslintrc.js new file mode 100644 index 0000000000..a070dc55ee --- /dev/null +++ b/packages/cli-module-build/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**', 'templates/**'], +}); diff --git a/packages/cli-module-build/README.md b/packages/cli-module-build/README.md new file mode 100644 index 0000000000..355891c907 --- /dev/null +++ b/packages/cli-module-build/README.md @@ -0,0 +1,23 @@ +# @backstage/cli-module-build + +CLI module that provides build, start, and packaging commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------- | :------------------------------------------------------------------------ | +| `package build` | Build a package for production deployment or publishing | +| `package start` | Start a package for local development | +| `package clean` | Delete cache directories | +| `package prepack` | Prepares a package for packaging before publishing | +| `package postpack` | Restores the changes made by the prepack command | +| `repo build` | Build packages in the project, excluding bundled app and backend packages | +| `repo start` | Starts packages in the repo for local development | +| `repo clean` | Delete cache and output directories | +| `build-workspace` | Builds a temporary dist workspace from the provided packages | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Build System](https://backstage.io/docs/tooling/cli/build-system) diff --git a/packages/cli-module-build/bin/backstage-cli-module-build b/packages/cli-module-build/bin/backstage-cli-module-build new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-build/bin/backstage-cli-module-build @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-build/catalog-info.yaml b/packages/cli-module-build/catalog-info.yaml new file mode 100644 index 0000000000..65715cce2a --- /dev/null +++ b/packages/cli-module-build/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-build + title: '@backstage/cli-module-build' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-build/cli-report.md b/packages/cli-module-build/cli-report.md new file mode 100644 index 0000000000..1c11022dc8 --- /dev/null +++ b/packages/cli-module-build/cli-report.md @@ -0,0 +1,156 @@ +## CLI Report file for "@backstage/cli-module-build" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-build` + +``` +Usage: @backstage/cli-module-build [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + build-workspace + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-build build-workspace` + +``` +Usage: @backstage/cli-module-build build-workspace [packages...] + +Options: + --always-pack + -h, --help +``` + +### `backstage-cli-module-build package` + +``` +Usage: @backstage/cli-module-build package [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + postpack + prepack + start +``` + +### `backstage-cli-module-build package build` + +``` +Usage: @backstage/cli-module-build package build + +Options: + --config + --minify + --module-federation + --role + --skip-build-dependencies + --stats + -h, --help +``` + +### `backstage-cli-module-build package clean` + +``` +Usage: @backstage/cli-module-build package clean + +Options: + -h, --help +``` + +### `backstage-cli-module-build package postpack` + +``` +Usage: @backstage/cli-module-build package postpack + +Options: + -h, --help +``` + +### `backstage-cli-module-build package prepack` + +``` +Usage: @backstage/cli-module-build package prepack + +Options: + -h, --help +``` + +### `backstage-cli-module-build package start` + +``` +Usage: @backstage/cli-module-build package start + +Options: + --check + --config + --entrypoint + --inspect + --inspect-brk + --link + --require + --role + -h, --help +``` + +### `backstage-cli-module-build repo` + +``` +Usage: @backstage/cli-module-build repo [options] [command] [command] + +Options: + -h, --help + +Commands: + build + clean + help [command] + start +``` + +### `backstage-cli-module-build repo build` + +``` +Usage: @backstage/cli-module-build repo build + +Options: + --all + --minify + --since + -h, --help +``` + +### `backstage-cli-module-build repo clean` + +``` +Usage: @backstage/cli-module-build repo clean + +Options: + -h, --help +``` + +### `backstage-cli-module-build repo start` + +``` +Usage: @backstage/cli-module-build repo start [packages...] + +Options: + --config + --inspect + --inspect-brk + --link + --plugin + --require + -h, --help +``` diff --git a/packages/cli-module-build/config/webpack-public-path.js b/packages/cli-module-build/config/webpack-public-path.js new file mode 100644 index 0000000000..3e14e96eb9 --- /dev/null +++ b/packages/cli-module-build/config/webpack-public-path.js @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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. + */ + +// This script is used to pick up and set the public path of the Webpack bundle +// at runtime. The meta tag is injected by the app build, but only present in +// the `index.html.tmpl` file. The runtime value of the meta tag is populated by +// the app backend, when it templates the final `index.html` file. +// +// This is needed for additional chunks to use the correct public path, and it +// is not possible to set the `__webpack_public_path__` variable outside of the +// build itself. The Webpack output also does not read any tags or +// similar, this seems to be the only way to dynamically configure the public +// path at runtime. +const el = document.querySelector('meta[name="backstage-public-path"]'); +const path = el?.getAttribute('content'); +if (path) { + __webpack_public_path__ = path; +} diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json new file mode 100644 index 0000000000..d13fa4bac0 --- /dev/null +++ b/packages/cli-module-build/package.json @@ -0,0 +1,106 @@ +{ + "name": "@backstage/cli-module-build", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-build" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "bin": "bin/backstage-cli-module-build", + "files": [ + "dist", + "bin", + "config", + "templates" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/module-federation-common": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@module-federation/enhanced": "^0.21.6", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", + "@rollup/plugin-commonjs": "^26.0.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-yaml": "^4.0.0", + "@rspack/core": "^1.4.11", + "@rspack/dev-server": "^1.1.4", + "@rspack/plugin-react-refresh": "^1.4.3", + "@swc/core": "^1.15.6", + "bfj": "^9.0.2", + "buffer": "^6.0.3", + "chalk": "^4.0.0", + "chokidar": "^3.3.1", + "cleye": "^2.3.0", + "cross-spawn": "^7.0.3", + "css-loader": "^6.5.1", + "ctrlc-windows": "^2.1.0", + "esbuild-loader": "^4.0.0", + "eslint-rspack-plugin": "^4.2.1", + "eslint-webpack-plugin": "^4.2.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "html-webpack-plugin": "^5.6.3", + "lodash": "^4.17.21", + "mini-css-extract-plugin": "^2.4.2", + "node-stdlib-browser": "^1.3.1", + "npm-packlist": "^5.0.0", + "p-queue": "^6.6.2", + "postcss": "^8.1.0", + "postcss-import": "^16.1.0", + "process": "^0.11.10", + "raw-loader": "^4.0.2", + "react-dev-utils": "^12.0.0-next.60", + "react-refresh": "^0.18.0", + "rollup": "^4.27.3", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-esbuild": "^6.1.1", + "rollup-plugin-postcss": "^4.0.0", + "rollup-pluginutils": "^2.8.2", + "shell-quote": "^1.8.1", + "style-loader": "^3.3.1", + "swc-loader": "^0.2.3", + "tar": "^7.5.6", + "ts-checker-rspack-plugin": "^1.1.5", + "ts-morph": "^24.0.0", + "util": "^0.12.3", + "webpack": "~5.105.0", + "webpack-dev-server": "^5.0.0", + "yml-loader": "^2.1.0", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/lodash": "^4.14.151", + "@types/npm-packlist": "^3.0.0", + "@types/shell-quote": "^1.7.5" + } +} diff --git a/packages/cli-module-build/report.api.md b/packages/cli-module-build/report.api.md new file mode 100644 index 0000000000..34634dd448 --- /dev/null +++ b/packages/cli-module-build/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-build" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-build/src/commands/buildWorkspace.ts b/packages/cli-module-build/src/commands/buildWorkspace.ts new file mode 100644 index 0000000000..0abd9e5307 --- /dev/null +++ b/packages/cli-module-build/src/commands/buildWorkspace.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { createDistWorkspace } from '../lib/packager'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + // Normalize legacy --alwaysYarnPack alias (a genuinely different name, not + // just a casing variant — type-flag handles camelCase/kebab-case natively) + const normalizedArgs = args.map(a => { + if (a === '--alwaysYarnPack') { + return '--always-pack'; + } + if (a.startsWith('--alwaysYarnPack=')) { + return `--always-pack${a.substring('--alwaysYarnPack'.length)}`; + } + return a; + }); + + const { + flags: { alwaysPack }, + _: positionals, + } = cli( + { + help: { ...info, usage: `${info.usage} [packages...]` }, + booleanFlagNegation: true, + parameters: ['', '[packages...]'], + flags: { + alwaysPack: { + type: Boolean, + description: + 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', + }, + }, + }, + undefined, + normalizedArgs, + ); + + const [dir, ...packages] = positionals; + + if (!(await fs.pathExists(dir))) { + throw new Error(`Target workspace directory doesn't exist, '${dir}'`); + } + + await createDistWorkspace(packages, { + targetDir: dir, + alwaysPack, + enableFeatureDetection: true, + }); +}; diff --git a/packages/cli/src/modules/build/commands/package/build/command.ts b/packages/cli-module-build/src/commands/package/build/command.ts similarity index 50% rename from packages/cli/src/modules/build/commands/package/build/command.ts rename to packages/cli-module-build/src/commands/package/build/command.ts index 017024c476..07c6e3a8d7 100644 --- a/packages/cli/src/modules/build/commands/package/build/command.ts +++ b/packages/cli-module-build/src/commands/package/build/command.ts @@ -14,54 +14,106 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import fs from 'fs-extra'; import { buildPackage, Output } from '../../../lib/builder'; -import { findRoleFromCommand } from '../../../../../lib/role'; +import { findRoleFromCommand } from '../../../lib/role'; import { BackstagePackageJson, PackageGraph, PackageRoles, } from '@backstage/cli-node'; -import { paths } from '../../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { buildFrontend } from '../../../lib/buildFrontend'; import { buildBackend } from '../../../lib/buildBackend'; import { isValidUrl } from '../../../lib/urls'; import chalk from 'chalk'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { + role, + minify, + skipBuildDependencies, + stats, + config, + moduleFederation, + }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + role: { + type: String, + description: 'Run the command with an explicit package role', + }, + minify: { + type: Boolean, + description: + 'Minify the generated code. Does not apply to app package (app is minified by default).', + }, + skipBuildDependencies: { + type: Boolean, + description: + 'Skip the automatic building of local dependencies. Applies to backend packages only.', + }, + stats: { + type: Boolean, + description: + 'If bundle stats are available, write them to the output directory. Applies to app packages only.', + }, + config: { + type: [String], + description: + 'Config files to load instead of app-config.yaml. Applies to app packages only.', + default: [], + }, + moduleFederation: { + type: Boolean, + description: + 'Build a package as a module federation remote. Applies to frontend plugin packages only.', + }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues): Promise { const webpack = process.env.LEGACY_WEBPACK_BUILD ? (require('webpack') as typeof import('webpack')) : undefined; - const role = await findRoleFromCommand(opts); + const resolvedRole = await findRoleFromCommand({ role }); - if (role === 'frontend' || role === 'backend') { - const configPaths = (opts.config as string[]).map(arg => { + if (resolvedRole === 'frontend' || resolvedRole === 'backend') { + const configPaths = config.map(arg => { if (isValidUrl(arg)) { return arg; } - return paths.resolveTarget(arg); + return targetPaths.resolve(arg); }); - if (role === 'frontend') { + if (resolvedRole === 'frontend') { return buildFrontend({ - targetDir: paths.targetDir, + targetDir: targetPaths.dir, configPaths, - writeStats: Boolean(opts.stats), + writeStats: Boolean(stats), webpack, }); } return buildBackend({ - targetDir: paths.targetDir, + targetDir: targetPaths.dir, configPaths, - skipBuildDependencies: Boolean(opts.skipBuildDependencies), - minify: Boolean(opts.minify), + skipBuildDependencies: Boolean(skipBuildDependencies), + minify: Boolean(minify), }); } let isModuleFederationRemote: boolean | undefined = undefined; - if ((role as string) === 'frontend-dynamic-container') { + if ((resolvedRole as string) === 'frontend-dynamic-container') { console.log( chalk.yellow( `⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`, @@ -69,22 +121,22 @@ export async function command(opts: OptionValues): Promise { ); isModuleFederationRemote = true; } - if (opts.moduleFederation) { + if (moduleFederation) { isModuleFederationRemote = true; } if (isModuleFederationRemote) { console.log('Building package as a module federation remote'); return buildFrontend({ - targetDir: paths.targetDir, + targetDir: targetPaths.dir, configPaths: [], - writeStats: Boolean(opts.stats), + writeStats: Boolean(stats), isModuleFederationRemote, webpack, }); } - const roleInfo = PackageRoles.getRoleInfo(role); + const roleInfo = PackageRoles.getRoleInfo(resolvedRole); const outputs = new Set(); @@ -99,13 +151,13 @@ export async function command(opts: OptionValues): Promise { } const packageJson = (await fs.readJson( - paths.resolveTarget('package.json'), + targetPaths.resolve('package.json'), )) as BackstagePackageJson; return buildPackage({ outputs, packageJson, - minify: Boolean(opts.minify), + minify: Boolean(minify), workspacePackages: await PackageGraph.listTargetPackages(), }); -} +}; diff --git a/packages/cli/src/modules/build/commands/package/start/index.ts b/packages/cli-module-build/src/commands/package/build/index.ts similarity index 94% rename from packages/cli/src/modules/build/commands/package/start/index.ts rename to packages/cli-module-build/src/commands/package/build/index.ts index 680fe9e11d..7121eac500 100644 --- a/packages/cli/src/modules/build/commands/package/start/index.ts +++ b/packages/cli-module-build/src/commands/package/build/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { command } from './command'; +export { default } from './command'; diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/.gitignore b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/.gitignore new file mode 100644 index 0000000000..eb67c88369 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/.gitignore @@ -0,0 +1 @@ +!dist* \ No newline at end of file diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/dist/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/package.json new file mode 100644 index 0000000000..5c2babcbc2 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/packages/foo-node/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scope/foo-node", + "version": "1.0.0", + "main": "dist/index.js" +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/dist/index.js new file mode 100644 index 0000000000..b76e7a9d93 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/dist/index.js @@ -0,0 +1 @@ +module.exports = { default: { $$type: '@backstage/BackendFeature' } }; diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/package.json new file mode 100644 index 0000000000..53ea4fbdb8 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-backend/package.json @@ -0,0 +1,8 @@ +{ + "name": "@scope/plugin-foo-backend", + "version": "1.0.0", + "main": "dist/index.js", + "dependencies": { + "@scope/plugin-foo-common": "1.0.0" + } +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/dist/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/package.json new file mode 100644 index 0000000000..868fad5eaf --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/backend/plugins/foo-common/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scope/plugin-foo-common", + "version": "1.0.0", + "main": "dist/index.js" +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/dist/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/package.json new file mode 100644 index 0000000000..1ae64d251d --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/packages/foo-web/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scope/foo-web", + "version": "1.0.0", + "main": "dist/index.js" +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/dist/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/package.json new file mode 100644 index 0000000000..80abb7d0b8 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo-react/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scope/plugin-foo-react", + "version": "1.0.0", + "main": "dist/index.js" +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/dist/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/package.json new file mode 100644 index 0000000000..e5be9d71e5 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/dist-workspace/frontend/plugins/foo/package.json @@ -0,0 +1,8 @@ +{ + "name": "@scope/plugin-foo", + "version": "1.0.0", + "main": "dist/index.js", + "dependencies": { + "@scope/plugin-foo-react": "1.0.0" + } +} diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/@mf-types/index.d.ts b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/@mf-types/index.d.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/@mf-types/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/index.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/index.js new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/index.js @@ -0,0 +1 @@ + diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/remoteEntry.js b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/remoteEntry.js new file mode 100644 index 0000000000..94f616153c --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/dist/remoteEntry.js @@ -0,0 +1 @@ +// Module Federation remote entry diff --git a/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/package.json b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/package.json new file mode 100644 index 0000000000..96efb68a2f --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "@scope/plugin-foo", + "version": "1.0.0", + "backstage": { + "role": "frontend-plugin", + "pluginId": "foo" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "exports": { + ".": {}, + "./alpha": {}, + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "module": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ] + } + }, + "dependencies": { + "@backstage/catalog-model": "^1.7.6" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/cli/src/modules/build/lib/packager/index.ts b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts similarity index 84% rename from packages/cli/src/modules/build/lib/packager/index.ts rename to packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts index 75f3fdf71d..e512e4ba83 100644 --- a/packages/cli/src/modules/build/lib/packager/index.ts +++ b/packages/cli-module-build/src/commands/package/bundle/__fixtures__/packed/frontend/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2026 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. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { createDistWorkspace } from './createDistWorkspace'; +export {}; diff --git a/packages/cli-module-build/src/commands/package/bundle/command.test.ts b/packages/cli-module-build/src/commands/package/bundle/command.test.ts new file mode 100644 index 0000000000..642e220224 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/command.test.ts @@ -0,0 +1,1126 @@ +/* + * Copyright 2026 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. + */ + +/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect", "expectPathExists"] }] */ + +import { createMockDirectory } from '@backstage/backend-test-utils'; +import chalk from 'chalk'; +import fs from 'fs-extra'; +import os from 'node:os'; +import { join as joinPath } from 'node:path'; + +import { targetPaths } from '@backstage/cli-common'; +import { + bundleCommand, + filterBundleConfigSchemas, + postProcessBundlePackageJson, +} from './command'; + +const fixturesDir = joinPath(__dirname, '__fixtures__'); + +const mockCreateDistWorkspace = jest.fn(); +const mockPackToDirectory = jest.fn(); +const mockBuildFrontend = jest.fn(); +const mockRun = jest.fn(); +const mockRunOutput = jest.fn(); +const mockListTargetPackages = jest.fn(); +const mockLoadConfigSchema = jest.fn(); +const mockCreateRequire = jest.fn(); + +// Mock external dependencies + +jest.mock('@backstage/cli-common', () => ({ + ...jest.requireActual('@backstage/cli-common'), + targetPaths: { + dir: '', + rootDir: '', + resolve: jest.fn(), + }, + run: (...args: unknown[]) => mockRun(...args), + runOutput: (...args: unknown[]) => mockRunOutput(...args), +})); + +jest.mock('../../../lib/packager', () => ({ + ...jest.requireActual('../../../lib/packager'), + createDistWorkspace: (...args: unknown[]) => mockCreateDistWorkspace(...args), + packToDirectory: (...args: unknown[]) => mockPackToDirectory(...args), +})); + +jest.mock('../../../lib/buildFrontend', () => ({ + buildFrontend: (...args: unknown[]) => mockBuildFrontend(...args), +})); + +jest.mock('@backstage/cli-node', () => { + const actual = jest.requireActual('@backstage/cli-node'); + return { + ...actual, + PackageGraph: Object.assign(actual.PackageGraph, { + listTargetPackages: (...args: unknown[]) => + mockListTargetPackages(...args), + }), + }; +}); + +jest.mock('@backstage/config-loader', () => ({ + loadConfigSchema: (...args: unknown[]) => mockLoadConfigSchema(...args), +})); + +jest.mock('node:module', () => ({ + ...jest.requireActual('node:module'), + createRequire: (...args: unknown[]) => mockCreateRequire(...args), +})); + +// Unit tests for exported pure functions (no mocks required) + +describe('postProcessBundlePackageJson', () => { + it('clears scripts and devDependencies', () => { + const pkg: Record = { + scripts: { build: 'tsc' }, + devDependencies: { typescript: '^5.0.0' }, + }; + postProcessBundlePackageJson(pkg, '/tmp', 'backend', undefined, {}, false); + expect(pkg.scripts).toEqual({}); + expect(pkg.devDependencies).toEqual({}); + }); + + it('sets bundleDependencies for backend plugins', () => { + const pkg: Record = {}; + postProcessBundlePackageJson(pkg, '/tmp', 'backend', undefined, {}, false); + expect(pkg.bundleDependencies).toBe(true); + }); + + it('does not set bundleDependencies for frontend plugins', () => { + const pkg: Record = {}; + postProcessBundlePackageJson(pkg, '/tmp', 'frontend', undefined, {}, true); + expect(pkg.bundleDependencies).toBeUndefined(); + }); + + it('sets MF entry points for frontend plugins', () => { + const pkg: Record = { + main: './dist/index.cjs.js', + exports: { '.': './dist/index.cjs.js' }, + module: './dist/index.esm.js', + typesVersions: { '*': { '*': ['dist/index.d.ts'] } }, + }; + postProcessBundlePackageJson(pkg, '/tmp', 'frontend', undefined, {}, false); + expect(pkg.main).toBe('./dist/remoteEntry.js'); + expect(pkg.exports).toBeUndefined(); + expect(pkg.module).toBeUndefined(); + expect(pkg.typesVersions).toBeUndefined(); + }); + + it('does not set MF entry points for backend plugins', () => { + const pkg: Record = { + main: './dist/index.cjs.js', + }; + postProcessBundlePackageJson(pkg, '/tmp', 'backend', undefined, {}, false); + expect(pkg.main).toBe('./dist/index.cjs.js'); + }); + + it('merges root resolutions and strips patch prefix', () => { + const pkg: Record = { + resolutions: { existing: '3.0.0' }, + }; + const rootResolutions = { + 'some-dep': '1.0.0', + 'patched-dep': 'patch:patched-dep@npm%3A2.0.0#./patch', + }; + postProcessBundlePackageJson( + pkg, + '/tmp', + 'backend', + rootResolutions, + {}, + true, + ); + expect(pkg.resolutions).toEqual({ + 'some-dep': '1.0.0', + 'patched-dep': '2.0.0', + existing: '3.0.0', + }); + }); + + it('does not merge resolutions when needsDependencies is false', () => { + const pkg: Record = {}; + postProcessBundlePackageJson( + pkg, + '/tmp', + 'backend', + { dep: '1.0.0' }, + {}, + false, + ); + expect(pkg.resolutions).toBeUndefined(); + }); +}); + +describe('filterBundleConfigSchemas', () => { + const mockDir = createMockDirectory(); + + beforeEach(() => { + mockCreateRequire.mockImplementation((p: string) => + jest + .requireActual('node:module') + .createRequire(p), + ); + }); + + afterEach(() => { + mockCreateRequire.mockReset(); + }); + + function writePluginTree( + pluginPkg: Record, + deps: Record> = {}, + ) { + const content: Record = { + 'package.json': JSON.stringify(pluginPkg), + }; + for (const [name, depPkg] of Object.entries(deps)) { + content[`node_modules/${name}/package.json`] = JSON.stringify(depPkg); + } + mockDir.setContent(content); + } + + function schemas(...names: string[]) { + return names.map(n => ({ packageName: n, value: {}, path: '' })); + } + + it('includes the plugin itself', () => { + writePluginTree({ name: '@scope/my-plugin', dependencies: {} }); + const result = filterBundleConfigSchemas( + schemas('@scope/my-plugin', '@other/unrelated'), + mockDir.path, + ); + expect(result.map(s => s.packageName)).toEqual(['@scope/my-plugin']); + }); + + it('includes third-party deps without backstage metadata', () => { + writePluginTree( + { name: '@scope/my-plugin', dependencies: { lodash: '^4.0.0' } }, + { lodash: { name: 'lodash', version: '4.17.21' } }, + ); + const result = filterBundleConfigSchemas( + schemas('@scope/my-plugin', 'lodash'), + mockDir.path, + ); + expect(result.map(s => s.packageName)).toContain('lodash'); + }); + + it('includes same-pluginId libraries', () => { + writePluginTree( + { + name: '@scope/my-plugin', + backstage: { role: 'backend-plugin', pluginId: 'foo' }, + dependencies: { '@scope/my-lib': '^1.0.0' }, + }, + { + '@scope/my-lib': { + name: '@scope/my-lib', + backstage: { role: 'node-library', pluginId: 'foo' }, + }, + }, + ); + const result = filterBundleConfigSchemas( + schemas('@scope/my-plugin', '@scope/my-lib'), + mockDir.path, + ); + expect(result.map(s => s.packageName)).toEqual([ + '@scope/my-plugin', + '@scope/my-lib', + ]); + }); + + it('excludes library with different pluginId', () => { + writePluginTree( + { + name: '@scope/my-plugin', + backstage: { role: 'backend-plugin', pluginId: 'foo' }, + dependencies: { '@scope/other-lib': '^1.0.0' }, + }, + { + '@scope/other-lib': { + name: '@scope/other-lib', + backstage: { role: 'node-library', pluginId: 'bar' }, + }, + }, + ); + const result = filterBundleConfigSchemas( + schemas('@scope/my-plugin', '@scope/other-lib'), + mockDir.path, + ); + expect(result.map(s => s.packageName)).toEqual(['@scope/my-plugin']); + }); + + it('recursively includes depended plugin/module schemas', () => { + writePluginTree( + { + name: '@scope/my-plugin', + dependencies: { '@scope/my-module': '^1.0.0' }, + }, + { + '@scope/my-module': { + name: '@scope/my-module', + backstage: { role: 'backend-plugin-module' }, + }, + }, + ); + const result = filterBundleConfigSchemas( + schemas('@scope/my-plugin', '@scope/my-module'), + mockDir.path, + ); + expect(result.map(s => s.packageName)).toEqual([ + '@scope/my-plugin', + '@scope/my-module', + ]); + }); +}); + +// Integration tests for the bundle command (require mocks) + +describe('bundle command', () => { + const mockDir = createMockDirectory(); + + const backendPluginDir = 'plugins/foo-backend'; + const backendPkg = { + name: '@scope/plugin-foo-backend', + version: '1.0.0', + backstage: { role: 'backend-plugin' }, + dependencies: {}, + }; + const backendMangledName = 'scope-plugin-foo-backend'; + + const frontendPluginDir = 'plugins/foo'; + const frontendPkg = { + name: '@scope/plugin-foo', + version: '1.0.0', + backstage: { role: 'frontend-plugin' }, + dependencies: {}, + }; + + const defaultRootPkg = { + name: 'root', + version: '1.0.0', + resolutions: { + 'some-dep': '1.0.0', + 'patched-dep': 'patch:patched-dep@npm%3A2.0.0#./patch', + }, + }; + + const defaultOpts = { + build: true, + install: true, + clean: false, + verbose: false, + outputDestination: undefined as string | undefined, + outputName: undefined as string | undefined, + prePackedDir: undefined as string | undefined, + }; + + function setupPlugin( + projectRelativeDir: string, + pkg: Record, + bundleName = 'bundle', + ) { + const pluginDir = joinPath(mockDir.path, projectRelativeDir); + const targetDir = joinPath(pluginDir, bundleName); + + mockDir.setContent({ + [joinPath(projectRelativeDir, 'package.json')]: JSON.stringify(pkg), + [joinPath(projectRelativeDir, 'yarn.lock')]: '# yarn.lock content', + 'package.json': JSON.stringify(defaultRootPkg), + 'yarn.lock': '# root yarn.lock', + 'tmp/.keep': '', + }); + targetPaths.dir = pluginDir; + targetPaths.rootDir = mockDir.path; + (targetPaths.resolve as jest.Mock).mockImplementation((...args: string[]) => + joinPath(targetPaths.dir, ...args), + ); + + return { pluginDir, targetDir, relDir: projectRelativeDir, bundleName }; + } + + function setupCreateDistWorkspaceMock( + pluginDir: string, + targets: { name: string; dir: string }[] = [], + invokeLogger = false, + ) { + const mainTarget = { name: backendPkg.name, dir: pluginDir }; + mockCreateDistWorkspace.mockImplementation(async (_pkgNames, opts) => { + if (invokeLogger && opts.logger) { + opts.logger.log(`Moving ${backendPkg.name} into dist workspace`); + opts.logger.warn('some dist workspace warning'); + } + await fs.copy( + joinPath(fixturesDir, 'dist-workspace', 'backend'), + opts.targetDir, + ); + return { targets: targets.length ? targets : [mainTarget] }; + }); + } + + function setupPackToDirectoryMock() { + mockPackToDirectory.mockImplementation( + async (opts: { targetDir: string; packageName: string }) => { + await fs.copy( + joinPath(fixturesDir, 'packed', 'frontend'), + opts.targetDir, + ); + const pkgPath = joinPath(opts.targetDir, 'package.json'); + const pkg = await fs.readJson(pkgPath); + pkg.name = opts.packageName; + await fs.writeJson(pkgPath, pkg); + }, + ); + } + + function setupRunMock() { + mockRun.mockImplementation( + ( + args: string[], + opts: { cwd: string; onStdout?: (d: Buffer) => void }, + ) => ({ + waitForExit: async () => { + if (!args.includes('update-lockfile')) { + await fs.ensureDir(joinPath(opts.cwd, 'node_modules')); + await fs.ensureDir(joinPath(opts.cwd, '.yarn')); + } + opts.onStdout?.(Buffer.from('mock yarn output\n')); + }, + }), + ); + } + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(os, 'tmpdir').mockReturnValue(joinPath(mockDir.path, 'tmp')); + mockLoadConfigSchema.mockResolvedValue({ + serialize: () => ({ schemas: [] }), + }); + mockRunOutput.mockResolvedValue('/mock-cache'); + setupRunMock(); + mockCreateRequire.mockImplementation((path: string) => + jest.requireActual('node:module').createRequire(path), + ); + jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + // Clear native Node module cache for files under mockDir to prevent + // cross-test contamination when real createRequire loads dist/index.js. + const nativeModule = + jest.requireActual('node:module'); + const nativeRequire = nativeModule.createRequire(__filename); + for (const key of Object.keys(nativeRequire.cache ?? {})) { + if (key.includes(mockDir.path)) { + delete nativeRequire.cache![key]; + } + } + jest.restoreAllMocks(); + }); + + async function expectPathExists(parts: string[], exists: boolean) { + expect(await fs.pathExists(joinPath(...parts))).toBe(exists); + } + + describe('validation', () => { + it('throws when backstage.role is missing', async () => { + setupPlugin(backendPluginDir, { + name: '@scope/plugin-foo-backend', + version: '1.0.0', + }); + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + 'does not have a backstage.role defined in package.json', + ); + }); + + it('throws when role is invalid', async () => { + setupPlugin(backendPluginDir, { + ...backendPkg, + backstage: { role: 'backend' }, + }); + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + `only supports: ${chalk.cyan('backend-plugin')}, ${chalk.cyan( + 'backend-plugin-module', + )}, ${chalk.cyan('frontend-plugin')}, ${chalk.cyan( + 'frontend-plugin-module', + )}`, + ); + }); + + it('throws when bundled is true', async () => { + setupPlugin(backendPluginDir, { ...backendPkg, bundled: true }); + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + 'not compatible with dynamic plugin bundling', + ); + }); + }); + + describe('backend', () => { + let ctx: ReturnType; + beforeEach(() => { + ctx = setupPlugin(backendPluginDir, backendPkg); + }); + + describe('via createDistWorkspace', () => { + beforeEach(() => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + }); + + it('should produce backend bundle when build=true', async () => { + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(backendPkg.name); + expect(pkg.bundleDependencies).toBe(true); + await expectPathExists([ctx.targetDir, '.gitignore'], true); + await expectPathExists([ctx.targetDir, '.yarnrc.yml'], true); + expect(mockCreateDistWorkspace).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ buildDependencies: true }), + ); + }); + + it.each(['null', 'undefined'])( + 'should not write yarnPath when yarn config returns "%s"', + async sentinel => { + mockRunOutput.mockImplementation( + (args: string[]): Promise => { + if (args.includes('yarnPath')) { + return Promise.resolve(sentinel); + } + return Promise.resolve('/mock-cache'); + }, + ); + await bundleCommand(defaultOpts); + const yarnrc = await fs.readFile( + joinPath(ctx.targetDir, '.yarnrc.yml'), + 'utf8', + ); + expect(yarnrc).not.toContain('yarnPath'); + expect(yarnrc).toContain('nodeLinker: node-modules'); + }, + ); + + it('should write yarnPath when yarn config returns a real path', async () => { + mockRunOutput.mockImplementation((args: string[]): Promise => { + if (args.includes('yarnPath')) { + return Promise.resolve('/home/user/.yarn/releases/yarn-3.8.1.cjs'); + } + return Promise.resolve('/mock-cache'); + }); + await bundleCommand(defaultOpts); + const yarnrc = await fs.readFile( + joinPath(ctx.targetDir, '.yarnrc.yml'), + 'utf8', + ); + expect(yarnrc).toContain( + 'yarnPath: /home/user/.yarn/releases/yarn-3.8.1.cjs', + ); + }); + + it('should pass buildDependencies=false when build=false', async () => { + await bundleCommand({ ...defaultOpts, build: false }); + expect(mockCreateDistWorkspace).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ buildDependencies: false }), + ); + }); + + it('should produce backend bundle for backend-plugin-module role', async () => { + ctx = setupPlugin(backendPluginDir, { + ...backendPkg, + backstage: { role: 'backend-plugin-module' }, + }); + setupCreateDistWorkspaceMock(ctx.pluginDir); + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(backendPkg.name); + expect(mockCreateDistWorkspace).toHaveBeenCalled(); + expect(mockPackToDirectory).not.toHaveBeenCalled(); + }); + + it('should assemble local deps into embedded/ and clean up .yarn', async () => { + const commonRelDir = 'plugins/foo-common'; + const commonDir = joinPath(mockDir.path, commonRelDir); + + ctx = setupPlugin(backendPluginDir, { + ...backendPkg, + dependencies: { '@scope/plugin-foo-common': 'workspace:^' }, + }); + setupCreateDistWorkspaceMock(ctx.pluginDir, [ + { name: backendPkg.name, dir: ctx.pluginDir }, + { name: '@scope/plugin-foo-common', dir: commonDir }, + ]); + + await bundleCommand(defaultOpts); + + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(backendPkg.name); + await expectPathExists( + [ctx.targetDir, 'embedded', commonRelDir, 'package.json'], + true, + ); + expect(pkg.resolutions['@scope/plugin-foo-common']).toBe( + `file:./embedded/${commonRelDir}`, + ); + await expectPathExists([ctx.targetDir, '.yarn'], false); + }); + + it('should log formatted packing output when distLogger is invoked', async () => { + setupCreateDistWorkspaceMock(ctx.pluginDir, [], true); + await bundleCommand(defaultOpts); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining(`Packing`), + ); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining(backendPkg.name), + ); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('some dist workspace warning'), + ); + }); + }); + + describe('via createDistWorkspace - failure', () => { + it('should clean up temp dir and propagate error on createDistWorkspace failure', async () => { + mockCreateDistWorkspace.mockRejectedValue(new Error('pack failed')); + const tempBase = joinPath(mockDir.path, 'tmp'); + jest.spyOn(os, 'tmpdir').mockReturnValue(tempBase); + + await expect(bundleCommand(defaultOpts)).rejects.toThrow('pack failed'); + + const entries = await fs.readdir(tempBase).catch(() => []); + expect( + entries.filter((e: string) => e.startsWith('bundle-workspace-')), + ).toHaveLength(0); + }); + }); + + describe('via --pre-packed-dir', () => { + beforeEach(() => { + mockCreateDistWorkspace.mockClear(); + }); + + it('should copy from pre-packed dir and assemble embedded', async () => { + const prePackedPath = joinPath(mockDir.path, 'pre-packed'); + await fs.copy( + joinPath(fixturesDir, 'dist-workspace', 'backend'), + prePackedPath, + ); + + mockListTargetPackages.mockResolvedValue([ + { packageJson: { name: backendPkg.name }, dir: ctx.pluginDir }, + ]); + + await bundleCommand({ ...defaultOpts, prePackedDir: prePackedPath }); + + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(backendPkg.name); + expect(pkg.bundleDependencies).toBe(true); + }); + + it('should print warning when package not found in pre-packed dir', async () => { + ctx = setupPlugin(backendPluginDir, { + ...backendPkg, + dependencies: { '@scope/other-dep': 'workspace:^' }, + }); + mockListTargetPackages.mockResolvedValue([ + { + packageJson: { + name: backendPkg.name, + version: '1.0.0', + dependencies: { '@scope/other-dep': 'workspace:^' }, + }, + dir: ctx.pluginDir, + }, + { + packageJson: { name: '@scope/other-dep', version: '1.0.0' }, + dir: joinPath(mockDir.path, 'packages/other-dep'), + }, + ]); + + const prePackedPath = joinPath(mockDir.path, 'pre-packed'); + await fs.copy( + joinPath(fixturesDir, 'dist-workspace', 'backend'), + prePackedPath, + ); + + await bundleCommand({ ...defaultOpts, prePackedDir: prePackedPath }); + + expect(console.warn).toHaveBeenCalledWith( + chalk.yellow( + ` Package ${chalk.cyan( + '@scope/other-dep', + )} not found in pre-packed dir (expected at ${chalk.cyan( + 'packages/other-dep', + )})`, + ), + ); + }); + }); + + describe('lockfile and install', () => { + beforeEach(() => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + }); + + it('should seed lockfile from monorepo root when plugin has no yarn.lock', async () => { + mockDir.setContent({ + [joinPath(backendPluginDir, 'package.json')]: + JSON.stringify(backendPkg), + 'package.json': JSON.stringify(defaultRootPkg), + 'yarn.lock': '# root yarn.lock content', + 'tmp/.keep': '', + }); + + await bundleCommand(defaultOpts); + + const lockContent = await fs.readFile( + joinPath(ctx.targetDir, 'yarn.lock'), + 'utf8', + ); + expect(lockContent).toBe('# root yarn.lock content'); + }); + + it('throws when no yarn.lock exists', async () => { + mockDir.setContent({ + [joinPath(backendPluginDir, 'package.json')]: + JSON.stringify(backendPkg), + 'package.json': JSON.stringify(defaultRootPkg), + 'tmp/.keep': '', + }); + + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + `Could not find a ${chalk.cyan( + 'yarn.lock', + )} file in either the plugin directory or the monorepo root (${chalk.cyan( + mockDir.path, + )})`, + ); + }); + + it('should run prune and install in the bundle dir', async () => { + await bundleCommand(defaultOpts); + expect(mockRun).toHaveBeenCalledWith( + expect.arrayContaining([ + 'yarn', + 'install', + '--no-immutable', + '--mode', + 'update-lockfile', + ]), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + expect(mockRun).toHaveBeenCalledWith( + expect.arrayContaining(['yarn', 'install', '--immutable']), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + }); + + it('throws when backend plugin has no valid BackendFeature export', async () => { + mockCreateRequire.mockImplementation(() => + Object.assign(() => ({ default: {} }), { + resolve: (id: string) => { + if (id.includes('package.json')) { + return joinPath(targetPaths.dir, 'node_modules', id); + } + throw new Error(`Cannot find module '${id}'`); + }, + }), + ); + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + 'Backend plugin is not valid for dynamic loading', + ); + }); + }); + + describe('--no-install', () => { + beforeEach(() => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + }); + + it('should skip install and print warning', async () => { + await bundleCommand({ ...defaultOpts, install: false }); + + await expectPathExists([ctx.targetDir, 'package.json'], true); + await expectPathExists([ctx.targetDir, 'yarn.lock'], true); + await expectPathExists([ctx.targetDir, 'node_modules'], false); + expect(mockRun).toHaveBeenCalledWith( + expect.arrayContaining(['--mode', 'update-lockfile']), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + expect(mockRun).not.toHaveBeenCalledWith( + expect.arrayContaining(['--immutable']), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Skipping dependency installation'), + ); + }); + }); + + describe('options', () => { + beforeEach(() => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + }); + + it('should remove target when clean=true', async () => { + mockDir.addContent({ + [joinPath(ctx.relDir, ctx.bundleName, 'existing-file')]: 'content', + }); + + await bundleCommand({ ...defaultOpts, clean: true }); + + await expectPathExists([ctx.targetDir, 'existing-file'], false); + await expectPathExists([ctx.targetDir, 'package.json'], true); + }); + + it('should write bundle to custom outputDestination with mangled name', async () => { + const customOutput = joinPath(mockDir.path, 'custom-output'); + await bundleCommand({ + ...defaultOpts, + outputDestination: customOutput, + }); + + await expectPathExists( + [customOutput, backendMangledName, 'package.json'], + true, + ); + }); + + it('should use "bundle" as default name when output stays in package dir', async () => { + await bundleCommand(defaultOpts); + const entries = await fs.readdir(ctx.pluginDir); + expect(entries).toContain('bundle'); + }); + + it('should use mangled package name when outputDestination is given', async () => { + const customOutput = joinPath(mockDir.path, 'custom-output'); + await bundleCommand({ + ...defaultOpts, + outputDestination: customOutput, + }); + const entries = await fs.readdir(customOutput); + expect(entries).toContain(backendMangledName); + }); + + it('should use explicit outputName when provided', async () => { + await bundleCommand({ ...defaultOpts, outputName: 'my-custom-bundle' }); + const entries = await fs.readdir(ctx.pluginDir); + expect(entries).toContain('my-custom-bundle'); + }); + + it('should pipe run output to console when verbose=true', async () => { + await bundleCommand({ ...defaultOpts, verbose: true }); + + expect(mockRun).toHaveBeenCalledWith( + expect.arrayContaining(['yarn', 'install', '--immutable']), + expect.anything(), + ); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('mock yarn output'), + ); + }); + + it('should write .bundle-output marker to the output directory', async () => { + await bundleCommand(defaultOpts); + await expectPathExists([ctx.targetDir, '.bundle-output'], true); + }); + + it('should remove nested dirs that have a .bundle-output marker', async () => { + // Leave a marked directory from a previous bundle run in the source tree. + const prevBundleDir = joinPath(ctx.pluginDir, 'old-bundle'); + await fs.ensureDir(prevBundleDir); + await fs.writeFile(joinPath(prevBundleDir, '.bundle-output'), ''); + + // Simulate yarn pack pulling the stale dir into the packed output. + mockCreateDistWorkspace.mockImplementation(async (_pkgNames, opts) => { + await fs.copy( + joinPath(fixturesDir, 'dist-workspace', 'backend'), + opts.targetDir, + ); + const nestedDir = joinPath(opts.targetDir, ctx.relDir, 'old-bundle'); + await fs.ensureDir(nestedDir); + await fs.writeFile(joinPath(nestedDir, 'stale-file'), ''); + return { targets: [{ name: backendPkg.name, dir: ctx.pluginDir }] }; + }); + + await bundleCommand(defaultOpts); + await expectPathExists([ctx.targetDir, 'old-bundle'], false); + }); + + it('should not create recursive nesting when run twice without --clean', async () => { + await bundleCommand(defaultOpts); + await bundleCommand(defaultOpts); + + const entries = await fs.readdir(ctx.targetDir); + expect(entries).not.toContain('bundle'); + }); + }); + + describe('error handling', () => { + it('should propagate error and show log when pruneBundleLockfile fails', async () => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + mockRun + .mockReturnValueOnce({ waitForExit: () => Promise.resolve() }) + .mockImplementationOnce((_args: any, opts: any) => ({ + waitForExit: async () => { + (opts?.onStdout ?? opts?.onStderr)?.( + Buffer.from('yarn prune output line 1\nline 2\n'), + ); + throw new Error('prune failed'); + }, + })); + + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + 'prune failed', + ); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Full log available at'), + ); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('--- last 20 lines ---'), + ); + }); + + it('should propagate error when installBundleDependencies fails', async () => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + let callCount = 0; + mockRun.mockImplementation(() => ({ + waitForExit: () => + ++callCount === 2 + ? Promise.reject(new Error('install failed')) + : Promise.resolve(), + })); + + await expect(bundleCommand(defaultOpts)).rejects.toThrow( + 'install failed', + ); + }); + }); + + describe('config schema', () => { + beforeEach(() => { + setupCreateDistWorkspaceMock(ctx.pluginDir); + }); + + it('should set configSchema in package.json when schemas are found', async () => { + mockLoadConfigSchema.mockResolvedValue({ + serialize: () => ({ + schemas: [ + { + packageName: backendPkg.name, + value: { + type: 'object', + properties: { foo: { type: 'string' } }, + }, + path: 'schemas/foo.json', + }, + ], + }), + }); + + await bundleCommand(defaultOpts); + + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.configSchema).toBe('dist/.config-schema.json'); + const schemaFile = await fs.readJson( + joinPath(ctx.targetDir, 'dist', '.config-schema.json'), + ); + expect(schemaFile.backstageConfigSchemaVersion).toBe(1); + expect(schemaFile.schemas).toHaveLength(1); + }); + + it('should not set configSchema when no schemas are found', async () => { + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.configSchema).toBeUndefined(); + }); + }); + }); + + describe('frontend', () => { + let ctx: ReturnType; + beforeEach(() => { + ctx = setupPlugin(frontendPluginDir, frontendPkg); + setupPackToDirectoryMock(); + }); + + describe('without --pre-packed-dir', () => { + it('should produce frontend bundle when build=true', async () => { + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(frontendPkg.name); + await expectPathExists([ctx.targetDir, '.gitignore'], true); + await expectPathExists([ctx.targetDir, '.yarnrc.yml'], false); + await expectPathExists([ctx.targetDir, 'yarn.lock'], false); + await expectPathExists([ctx.targetDir, 'node_modules'], false); + await expectPathExists([ctx.targetDir, 'src'], false); + expect(mockBuildFrontend).toHaveBeenCalled(); + expect(mockPackToDirectory).toHaveBeenCalled(); + expect(mockCreateDistWorkspace).not.toHaveBeenCalled(); + }); + + it('should keep src/ when "files" explicitly includes it', async () => { + ctx = setupPlugin(frontendPluginDir, { + ...frontendPkg, + files: ['dist', 'src'], + }); + setupPackToDirectoryMock(); + await bundleCommand(defaultOpts); + await expectPathExists([ctx.targetDir, 'src'], true); + }); + + it('should produce frontend bundle when build=false', async () => { + await bundleCommand({ ...defaultOpts, build: false }); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(frontendPkg.name); + await expectPathExists([ctx.targetDir, 'yarn.lock'], false); + await expectPathExists([ctx.targetDir, 'node_modules'], false); + expect(mockBuildFrontend).not.toHaveBeenCalled(); + expect(mockPackToDirectory).toHaveBeenCalled(); + }); + + it('should produce frontend bundle for frontend-plugin-module role', async () => { + ctx = setupPlugin(frontendPluginDir, { + ...frontendPkg, + backstage: { role: 'frontend-plugin-module' }, + }); + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.name).toBe(frontendPkg.name); + expect(mockBuildFrontend).toHaveBeenCalled(); + expect(mockPackToDirectory).toHaveBeenCalled(); + expect(mockCreateDistWorkspace).not.toHaveBeenCalled(); + }); + }); + + describe('package.json post-processing', () => { + it('should apply frontend-specific and common post-processing', async () => { + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.main).toBe('./dist/remoteEntry.js'); + expect(pkg.types).toBe('./dist/@mf-types/index.d.ts'); + expect(pkg.exports).toBeUndefined(); + expect(pkg.module).toBeUndefined(); + expect(pkg.typesVersions).toBeUndefined(); + expect(pkg.dependencies).toEqual({ + '@backstage/catalog-model': '^1.7.6', + }); + expect(pkg.keywords).toEqual(['backstage']); + expect(pkg.license).toBe('Apache-2.0'); + expect(pkg.backstage).toEqual( + expect.objectContaining({ role: 'frontend-plugin' }), + ); + expect(pkg.bundleDependencies).toBeUndefined(); + expect(pkg.scripts).toEqual({}); + expect(pkg.devDependencies).toEqual({}); + }); + + it('should delete types when @mf-types/index.d.ts is absent', async () => { + mockPackToDirectory.mockImplementation( + async (opts: { targetDir: string; packageName: string }) => { + await fs.copy( + joinPath(fixturesDir, 'packed', 'frontend'), + opts.targetDir, + ); + await fs.remove( + joinPath(opts.targetDir, 'dist', '@mf-types', 'index.d.ts'), + ); + const pkgPath = joinPath(opts.targetDir, 'package.json'); + const pkg = await fs.readJson(pkgPath); + pkg.name = opts.packageName; + await fs.writeJson(pkgPath, pkg); + }, + ); + await bundleCommand(defaultOpts); + const pkg = await fs.readJson(joinPath(ctx.targetDir, 'package.json')); + expect(pkg.main).toBe('./dist/remoteEntry.js'); + expect(pkg.types).toBeUndefined(); + }); + }); + + describe('with --pre-packed-dir', () => { + beforeEach(async () => { + mockListTargetPackages.mockResolvedValue([ + { packageJson: { name: frontendPkg.name }, dir: ctx.pluginDir }, + ]); + const prePackedPath = joinPath(mockDir.path, 'pre-packed'); + await fs.copy( + joinPath(fixturesDir, 'dist-workspace', 'frontend'), + prePackedPath, + ); + mockBuildFrontend.mockImplementation(async () => { + const distDir = joinPath(ctx.pluginDir, 'dist'); + await fs.ensureDir(distDir); + await fs.writeFile( + joinPath(distDir, 'remoteEntry.js'), + '// MF remote entry', + ); + }); + }); + + it('should copy from pre-packed and prune lockfile but not install', async () => { + await bundleCommand({ + ...defaultOpts, + prePackedDir: joinPath(mockDir.path, 'pre-packed'), + }); + + await expectPathExists([ctx.targetDir, 'package.json'], true); + await expectPathExists([ctx.targetDir, 'yarn.lock'], true); + await expectPathExists([ctx.targetDir, 'node_modules'], false); + await expectPathExists([ctx.targetDir, 'dist', 'remoteEntry.js'], true); + expect(mockRun).toHaveBeenCalledWith( + expect.arrayContaining(['--mode', 'update-lockfile']), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + expect(mockRun).not.toHaveBeenCalledWith( + expect.arrayContaining(['--immutable']), + expect.objectContaining({ cwd: ctx.targetDir }), + ); + }); + }); + + describe('error handling', () => { + it('should propagate error when packToDirectory fails', async () => { + mockPackToDirectory.mockRejectedValue(new Error('pack failed')); + + await expect(bundleCommand(defaultOpts)).rejects.toThrow('pack failed'); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Full log available at'), + ); + }); + + it('should not show last 20 lines when verbose=true on failure', async () => { + mockPackToDirectory.mockRejectedValue(new Error('pack failed')); + + await expect( + bundleCommand({ ...defaultOpts, verbose: true }), + ).rejects.toThrow('pack failed'); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Full log available at'), + ); + expect(console.error).not.toHaveBeenCalledWith( + expect.stringContaining('--- last 20 lines ---'), + ); + }); + }); + }); +}); diff --git a/packages/cli-module-build/src/commands/package/bundle/command.ts b/packages/cli-module-build/src/commands/package/bundle/command.ts new file mode 100644 index 0000000000..ee88322733 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/command.ts @@ -0,0 +1,979 @@ +/* + * Copyright 2026 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 { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { run, runOutput } from '@backstage/cli-common'; +import chalk from 'chalk'; +import { cli } from 'cleye'; +import fs from 'fs-extra'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import { + join as joinPath, + resolve as resolvePath, + relative as relativePath, +} from 'node:path'; + +import { loadConfigSchema } from '@backstage/config-loader'; +import { targetPaths } from '@backstage/cli-common'; +import { buildFrontend } from '../../../lib/buildFrontend'; +import { + createDistWorkspace, + packToDirectory, + resolveLocalDependencies, +} from '../../../lib/packager'; +import type { CliCommandContext } from '@backstage/cli-node'; + +interface BundleOptions { + build: boolean; + install: boolean; + clean: boolean; + verbose: boolean; + outputDestination?: string; + outputName?: string; + prePackedDir?: string; +} + +/** + * Bundle a plugin for dynamic loading. + * + * This creates a self-contained plugin bundle that can be deployed independently + * and loaded dynamically by a Backstage application. Supports both backend and + * frontend plugins. + * + * For backend plugins, `createDistWorkspace` handles building (CJS) and packing + * all local dependencies. The output is restructured so that the main plugin + * sits at the bundle root and its local dependencies live under `embedded/`. + * A lockfile is seeded, pruned, and used to install a private `node_modules`. + * + * For frontend plugins, a module federation remote build produces the final + * assets. Only the main plugin is packed into the bundle root (no `embedded/`, + * no lockfile, no `node_modules`). + * + * When `--pre-packed-dir` is provided, local dependencies are copied from a + * pre-built dist workspace instead of calling `createDistWorkspace`: + * - For backend plugins this is a performance optimization when many plugins from the same monorepo are being bundled. + * - For frontend plugins it additionally enables lockfile generation (seed + prune) for dependency tracking purposes such as SBOM generation. + * - The pre-built dist workspace is produced by + * `backstage-cli build-workspace [packages...] --alwaysPack` + * and `` is then passed as `--pre-packed-dir`. + * - The `--alwaysPack` flag is required so that `workspace:^` and `backstage:^` + * dependency specs are resolved to concrete versions in the packed output. + */ +export async function bundleCommand(opts: BundleOptions): Promise { + const pkgJsonPath = targetPaths.resolve('package.json'); + const pkg = (await fs.readJson(pkgJsonPath)) as BackstagePackageJson; + + const outputDestination = opts.outputDestination + ? resolvePath(opts.outputDestination) + : targetPaths.dir; + const mangledName = pkg.name.replace(/^@/, '').replace(/\//, '-'); + let bundleName = 'bundle'; + if (opts.outputName) { + bundleName = opts.outputName; + } else if (opts.outputDestination) { + bundleName = mangledName; + } + const target = joinPath(outputDestination, bundleName); + + const role = pkg.backstage?.role; + if (!role) { + throw new Error( + `Package ${chalk.cyan( + pkg.name, + )} does not have a backstage.role defined in package.json`, + ); + } + + const validRoles = [ + 'backend-plugin', + 'backend-plugin-module', + 'frontend-plugin', + 'frontend-plugin-module', + ]; + if (!validRoles.includes(role)) { + throw new Error( + `Package ${chalk.cyan(pkg.name)} has role ${chalk.cyan( + role, + )}, but bundle command ` + + `only supports: ${validRoles.map(r => chalk.cyan(r)).join(', ')}`, + ); + } + + const pluginType: 'frontend' | 'backend' = + role === 'frontend-plugin' || role === 'frontend-plugin-module' + ? 'frontend' + : 'backend'; + + if (pkg.bundled) { + throw new Error( + `Package ${chalk.cyan(pkg.name)} has ${chalk.cyan( + 'bundled: true', + )} which is not ` + `compatible with dynamic plugin bundling.`, + ); + } + + console.log( + chalk.blue(`Bundling ${chalk.cyan(pkg.name)} for dynamic loading...`), + ); + console.log(`${chalk.dim('Output:')} ${chalk.cyan(target)}`); + + if (opts.clean) { + console.log(chalk.blue(`Cleaning ${chalk.cyan(target)}`)); + await fs.remove(target); + } + + await fs.mkdirs(target); + + await fs.writeFile(joinPath(target, '.gitignore'), '*\n'); + await fs.writeFile(joinPath(target, '.bundle-output'), ''); + + const rootPkg = await fs.readJson( + resolvePath(targetPaths.rootDir, 'package.json'), + ); + + // Backend plugins always need embedded packages, lockfile, and node_modules. + // Frontend plugins need them only when --pre-packed-dir is provided. + const needsDependencies = pluginType === 'backend' || !!opts.prePackedDir; + + // Establish the bundle directory as its own Yarn project root so that + // the seeded yarn.lock is the one Yarn reads/writes, even when the + // output directory is inside another monorepo. + // Only needed when lockfile/install operations will run. + if (needsDependencies) { + const yarnrcLines = ['nodeLinker: node-modules']; + try { + // Include yarnPath so the same Yarn version that created the lockfile + // is used for pruning/installing -- lockfile formats differ across + // major Yarn versions (e.g. ~builtin vs optional!builtin patches). + const resolved = await runOutput(['yarn', 'config', 'get', 'yarnPath'], { + cwd: targetPaths.rootDir, + }); + const yarnPathSentinels = new Set(['undefined', 'null']); + if (resolved && !yarnPathSentinels.has(resolved)) { + yarnrcLines.push(`yarnPath: ${resolved}`); + } + } catch { + // yarnPath not configured — check if corepack manages the version instead + if (!rootPkg.packageManager) { + console.warn( + chalk.yellow( + 'No yarnPath configured and no packageManager field found. ' + + 'The Yarn version in PATH will be used for lockfile operations.', + ), + ); + } + } + + await fs.writeFile( + joinPath(target, '.yarnrc.yml'), + `${yarnrcLines.join('\n')}\n`, + ); + } + + // ── Step 0 (frontend only): Module federation build ───────────────── + if (pluginType === 'frontend' && opts.build) { + console.log(chalk.blue('Building module federation remote...')); + await buildFrontend({ + targetDir: targetPaths.dir, + configPaths: [], + writeStats: false, + isModuleFederationRemote: true, + }); + } + + const embeddedResolutions: Record = {}; + + // Detect previous bundle output directories inside the source package so + // they can be stripped from the yarn-pack result later. npm-packlist's + // basename matching on the `files` field picks up identically-named entries + // from prior bundle outputs, creating nested copies. + const bundleOutputDirs: string[] = []; + try { + const entries = await fs.readdir(targetPaths.dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + if ( + await fs.pathExists( + joinPath(targetPaths.dir, entry.name, '.bundle-output'), + ) + ) { + bundleOutputDirs.push(entry.name); + } + } + } + } catch { + /* directory may not exist yet on first run */ + } + + if (needsDependencies) { + // ── Step 1: Populate embedded packages ────────────────────────────── + const embeddedDir = joinPath(target, 'embedded'); + let targets: { name: string; dir: string }[]; + + if (opts.prePackedDir) { + // ── Strategy A: Copy from pre-built dist workspace ─────────────── + // Reuses output from `backstage-cli build-workspace --alwaysPack`. + // Works for both backend (performance optimization) and frontend + // (enables lockfile generation for SBOM). + const prePackedDir = resolvePath(opts.prePackedDir); + console.log( + chalk.blue( + `Using pre-packed workspace at ${chalk.cyan(prePackedDir)}...`, + ), + ); + + const packages = await PackageGraph.listTargetPackages(); + targets = await resolveLocalDependencies([pkg.name], packages); + await fs.ensureDir(embeddedDir); + + for (const dep of targets) { + const relDir = relativePath(targetPaths.rootDir, dep.dir); + const srcDir = resolvePath(prePackedDir, relDir); + const destDir = resolvePath(embeddedDir, relDir); + if (await fs.pathExists(srcDir)) { + await fs.copy(srcDir, destDir); + } else { + console.warn( + chalk.yellow( + ` Package ${chalk.cyan(dep.name)} not found in pre-packed ` + + `dir (expected at ${chalk.cyan(relDir)})`, + ), + ); + } + } + } else { + // ── Strategy B: createDistWorkspace ────────────────────────────── + // Pack all local dependencies into a temp directory first, then + // move the result into target/embedded/. We must NOT pack directly + // into the target tree because yarn pack's basename-matching on the + // `files` field would pick up identically-named files from + // already-extracted embedded packages. + const tempWorkspaceDir = await fs.mkdtemp( + joinPath(tmpdir(), 'bundle-workspace-'), + ); + console.log(chalk.blue('Packing local dependencies...')); + + const distLog = createStepLogger( + joinPath(target, 'dist-workspace.log'), + opts.verbose, + ); + + const packingPattern = /^(?:Moving|Repacking) (.+) into dist workspace$/; + const distLogger = { + log(msg: string) { + const match = msg.match(packingPattern); + if (match) { + console.log(` ${chalk.dim('Packing')} ${chalk.cyan(match[1])}`); + } + distLog.logger.log(msg); + }, + warn(msg: string) { + console.warn(` ${chalk.yellow(msg)}`); + distLog.logger.warn(msg); + }, + }; + + try { + ({ targets } = await createDistWorkspace([pkg.name], { + targetDir: tempWorkspaceDir, + files: [], + alwaysPack: true, + buildDependencies: opts.build, + buildExcludes: opts.build ? [] : undefined, + logger: distLogger, + })); + await fs.remove(embeddedDir); + await fs.move(tempWorkspaceDir, embeddedDir); + } catch (err) { + await distLog.close(); + await showLogOnError(distLog.path, opts.verbose); + throw err; + } finally { + if (await fs.pathExists(tempWorkspaceDir)) { + await fs.remove(tempWorkspaceDir); + } + } + await distLog.close(); + await fs.remove(distLog.path); + } + + // ── Step 2: Assemble embedded packages ────────────────────────────── + const mainPluginRelDir = relativePath(targetPaths.rootDir, targetPaths.dir); + const mainPluginEmbeddedDir = resolvePath(embeddedDir, mainPluginRelDir); + + console.log( + chalk.blue( + `Moving main plugin ${chalk.cyan(pkg.name)} to bundle root...`, + ), + ); + + if (!(await fs.pathExists(mainPluginEmbeddedDir))) { + throw new Error( + `Main plugin ${chalk.cyan(pkg.name)} was not found in the ` + + `embedded workspace at ${chalk.cyan(mainPluginRelDir)}. ` + + `Ensure the pre-packed workspace includes this plugin.`, + ); + } + + const mainPluginEntries = await fs.readdir(mainPluginEmbeddedDir); + for (const entry of mainPluginEntries) { + await fs.move( + joinPath(mainPluginEmbeddedDir, entry), + joinPath(target, entry), + { overwrite: true }, + ); + } + + await fs.remove(mainPluginEmbeddedDir); + + // For frontend plugins, the pre-packed dist/ contains standard CJS + // output, not Module Federation artifacts. Overlay the MF build + // output from the source directory (produced by Step 0). + if (pluginType === 'frontend') { + const sourceDist = resolvePath(targetPaths.dir, 'dist'); + if (await fs.pathExists(sourceDist)) { + await fs.copy(sourceDist, joinPath(target, 'dist'), { + overwrite: true, + }); + } + } + + const localDeps = targets.filter(t => t.name !== pkg.name); + + for (const dep of localDeps) { + const depRelDir = relativePath(targetPaths.rootDir, dep.dir); + const depEmbeddedDir = resolvePath(embeddedDir, depRelDir); + + if (!(await fs.pathExists(depEmbeddedDir))) { + continue; + } + + embeddedResolutions[dep.name] = `file:./embedded/${depRelDir}`; + } + + if (Object.keys(embeddedResolutions).length === 0) { + await fs.remove(embeddedDir); + } + } else { + // ── Step 1b: Pack main plugin only ────────────────────────────────── + // Frontend plugins without --pre-packed-dir don't need transitive + // local deps -- just pack the main plugin directly into the bundle root. + console.log(chalk.blue(`Packing main plugin ${chalk.cyan(pkg.name)}...`)); + + const distLog = createStepLogger( + joinPath(target, 'dist-workspace.log'), + opts.verbose, + ); + + try { + await packToDirectory({ + packageDir: targetPaths.dir, + packageName: pkg.name, + targetDir: target, + logger: distLog.logger, + }); + } catch (err) { + await distLog.close(); + await showLogOnError(distLog.path, opts.verbose); + throw err; + } + await distLog.close(); + await fs.remove(distLog.path); + } + + // Remove any previous bundle output directories that were erroneously + // included by yarn pack (npm-packlist's basename matching on the `files` + // field picks up identically-named entries from prior bundle outputs). + for (const dir of bundleOutputDirs) { + const nestedPath = joinPath(target, dir); + if (await fs.pathExists(nestedPath)) { + await fs.remove(nestedPath); + } + } + + // Remove src/ directory included by yarn pack because prepack's + // rewriteEntryPoints cannot rewrite main/types away from src/ paths + // when the standard dist output files are absent (MF builds). + // Skip if the package explicitly ships src/ via the "files" field. + if (pluginType === 'frontend') { + const srcExplicitlyIncluded = (pkg.files ?? []).some( + f => f === 'src' || f.startsWith('src/'), + ); + if (!srcExplicitlyIncluded) { + const srcPath = joinPath(target, 'src'); + if (await fs.pathExists(srcPath)) { + await fs.remove(srcPath); + } + } + } + + // ── Step 3: Config schema ──────────────────────────────────────────── + console.log(chalk.blue('Filtering config schema...')); + + const schemaPath = resolvePath(target, 'dist', '.config-schema.json'); + let schemas: Array<{ packageName: string }> = []; + + if (pluginType === 'frontend' && (await fs.pathExists(schemaPath))) { + const existing = await fs.readJson(schemaPath); + schemas = existing.schemas ?? []; + } else { + const configSchema = await loadConfigSchema({ + dependencies: [], + packagePaths: ['package.json'], + }); + const serialized = configSchema.serialize() as { + schemas: typeof schemas; + }; + schemas = serialized.schemas ?? []; + } + + let schemaWritten = false; + if (schemas.length > 0) { + const filtered = filterBundleConfigSchemas(schemas, targetPaths.dir); + if (filtered.length > 0) { + await fs.ensureDir(resolvePath(target, 'dist')); + await fs.writeJson( + schemaPath, + { backstageConfigSchemaVersion: 1, schemas: filtered }, + { spaces: 2 }, + ); + schemaWritten = true; + } else { + console.log( + chalk.dim(' No config schemas found for this plugin bundle'), + ); + } + } else { + console.log(chalk.dim(' No config schemas found for this plugin bundle')); + } + + // ── Step 4: Post-process package.json ──────────────────────────────── + console.log( + chalk.blue( + `Customizing ${chalk.cyan('package.json')} for dynamic loading...`, + ), + ); + + const targetPkgPath = resolvePath(target, 'package.json'); + const targetPkg = await fs.readJson(targetPkgPath); + + postProcessBundlePackageJson( + targetPkg, + target, + pluginType, + needsDependencies ? rootPkg?.resolutions : undefined, + embeddedResolutions, + needsDependencies, + ); + + if (schemaWritten) { + targetPkg.configSchema = 'dist/.config-schema.json'; + } + + await fs.writeJson(targetPkgPath, targetPkg, { spaces: 2 }); + + // ── Step 5: Seed lockfile, prune, & install ────────────────────────── + // Runs for backend plugins (always) and frontend plugins with + // --pre-packed-dir (for SBOM lockfile generation). + if (needsDependencies) { + await seedBundleLockfile(target, targetPaths.dir, targetPaths.rootDir); + + const sourceCacheFolder = await runOutput( + ['yarn', 'config', 'get', 'cacheFolder'], + { cwd: targetPaths.rootDir }, + ); + await pruneBundleLockfile(target, opts.verbose, sourceCacheFolder); + + if (pluginType === 'backend') { + if (opts.install) { + await installBundleDependencies(target, opts.verbose); + + // Clean up .yarn directory created during install + const yarnDir = joinPath(target, '.yarn'); + if (await fs.pathExists(yarnDir)) { + await fs.remove(yarnDir); + } + + console.log(chalk.blue('Validating plugin entry points...')); + + // Temporarily patch module resolution so the plugin can resolve + // itself by name (needed by resolvePackagePath at module load + // time, e.g. for database migration paths). At runtime this is + // handled by CommonJSModuleLoader in backend-dynamic-feature-service. + const NodeModule = + require('node:module') as typeof import('node:module') & { + _resolveFilename: Function; + }; + const origResolveFilename = NodeModule._resolveFilename; + NodeModule._resolveFilename = (request: string, ...args: any[]) => { + if (request === `${targetPkg.name}/package.json`) { + return resolvePath(target, 'package.json'); + } + return origResolveFilename(request, ...args); + }; + + try { + const pluginRequire = createRequire(`${target}/package.json`); + const mainModule = pluginRequire(target); + const alphaPath = resolvePath(target, 'alpha'); + const alphaModule = (await fs.pathExists(alphaPath)) + ? pluginRequire(alphaPath) + : undefined; + + const isBackendFeature = (v: unknown) => + !!v && + (typeof v === 'object' || typeof v === 'function') && + (v as { $$type?: string }).$$type === '@backstage/BackendFeature'; + + const hasValidExport = [mainModule, alphaModule] + .filter(Boolean) + .some(m => isBackendFeature(m?.default)); + + if (!hasValidExport) { + throw new Error( + `Backend plugin is not valid for dynamic loading: ` + + `it must export a ${chalk.cyan( + 'BackendFeature', + )} as default export`, + ); + } + } finally { + NodeModule._resolveFilename = origResolveFilename; + } + } else { + console.log( + chalk.yellow( + 'Skipping dependency installation and validation. ' + + 'Run without --no-install to validate the bundle.', + ), + ); + } + } + } + + console.log(chalk.green(`Bundle created at ${chalk.cyan(target)}`)); +} + +/** + * Mutates `targetPkg` in place to prepare it for dynamic plugin loading: + * clears scripts/devDependencies, applies plugin-type-specific adjustments + * (MF entry points for frontend, bundleDependencies for backend), and merges + * root + embedded resolutions when a lockfile is involved. + */ +export function postProcessBundlePackageJson( + targetPkg: Record, + targetDir: string, + pluginType: 'frontend' | 'backend', + rootResolutions: Record | undefined, + embeddedResolutions: Record, + needsDependencies: boolean, +): void { + targetPkg.scripts = {}; + targetPkg.devDependencies = {}; + + if (pluginType === 'frontend') { + targetPkg.main = './dist/remoteEntry.js'; + + const mfTypesIndex = resolvePath( + targetDir, + 'dist', + '@mf-types', + 'index.d.ts', + ); + if (fs.pathExistsSync(mfTypesIndex)) { + targetPkg.types = './dist/@mf-types/index.d.ts'; + } else { + delete targetPkg.types; + } + + delete targetPkg.exports; + delete targetPkg.module; + delete targetPkg.typesVersions; + } else if (pluginType === 'backend') { + targetPkg.bundleDependencies = true; + } + + if (needsDependencies) { + const patchVersionPattern = /^patch:.+@npm%3A([^#]+)#/; + const stripped: Record = {}; + if (rootResolutions) { + for (const [key, value] of Object.entries(rootResolutions)) { + if (typeof value !== 'string') { + continue; + } + const patchMatch = value.match(patchVersionPattern); + stripped[key] = patchMatch ? patchMatch[1] : value; + } + } + + targetPkg.resolutions = { + ...stripped, + ...(targetPkg.resolutions as Record | undefined), + ...embeddedResolutions, + }; + } +} + +/** + * Seeds the bundle's yarn.lock from the source plugin or monorepo lockfile. + * Looks first for a local yarn.lock in the plugin directory, then falls back + * to the monorepo root. + */ +async function seedBundleLockfile( + targetDir: string, + pluginDir: string, + monorepoRoot: string, +): Promise { + let sourceYarnLock: string | undefined; + if (await fs.pathExists(joinPath(pluginDir, 'yarn.lock'))) { + sourceYarnLock = joinPath(pluginDir, 'yarn.lock'); + } else if (await fs.pathExists(joinPath(monorepoRoot, 'yarn.lock'))) { + sourceYarnLock = joinPath(monorepoRoot, 'yarn.lock'); + } + + if (!sourceYarnLock) { + throw new Error( + `Could not find a ${chalk.cyan( + 'yarn.lock', + )} file in either the plugin directory or the monorepo root (${chalk.cyan( + monorepoRoot, + )})`, + ); + } + + const isMonorepoLock = sourceYarnLock === joinPath(monorepoRoot, 'yarn.lock'); + console.log( + chalk.blue( + `Seeding bundle ${chalk.cyan('yarn.lock')} from source plugin${ + isMonorepoLock ? ' monorepo' : '' + } lockfile...`, + ), + ); + + await fs.copyFile(sourceYarnLock, resolvePath(targetDir, 'yarn.lock')); +} + +/** + * Prunes the bundle's yarn.lock to remove entries not required by the + * bundle's package.json. Runs offline to avoid network access. + */ +async function pruneBundleLockfile( + targetDir: string, + verbose: boolean, + sourceCacheFolder: string, +): Promise { + console.log( + chalk.blue( + `Pruning bundle ${chalk.cyan( + 'yarn.lock', + )} to remove unused dependencies...`, + ), + ); + + const pruneLog = createStepLogger( + joinPath(targetDir, 'lockfile-prune.log'), + verbose, + '[lockfile-prune] ', + ); + try { + await run( + ['yarn', 'install', '--no-immutable', '--mode', 'update-lockfile'], + { + cwd: targetDir, + env: { + YARN_ENABLE_GLOBAL_CACHE: 'false', + YARN_ENABLE_NETWORK: '0', + YARN_ENABLE_MIRROR: 'false', + YARN_CACHE_FOLDER: sourceCacheFolder, + }, + onStdout: pruneLog.logRunOutput('out'), + onStderr: pruneLog.logRunOutput('err'), + }, + ).waitForExit(); + } catch (err) { + await pruneLog.close(); + await showLogOnError(pruneLog.path, verbose); + throw err; + } + await pruneLog.close(); + await fs.remove(pruneLog.path); +} + +/** + * Installs the bundle's dependencies using an immutable lockfile. + * This creates the node_modules directory needed for backend plugin runtime. + */ +async function installBundleDependencies( + targetDir: string, + verbose: boolean, +): Promise { + console.log(chalk.blue('Installing private dependencies...')); + + const installLog = createStepLogger( + joinPath(targetDir, 'yarn-install.log'), + verbose, + '[yarn-install] ', + ); + try { + await run(['yarn', 'install', '--immutable'], { + cwd: targetDir, + onStdout: installLog.logRunOutput('out'), + onStderr: installLog.logRunOutput('err'), + }).waitForExit(); + } catch (err) { + await installLog.close(); + await showLogOnError(installLog.path, verbose); + throw err; + } + await installLog.close(); + await fs.remove(installLog.path); +} + +/** + * Filters config schemas to keep only those belonging to the plugin's own + * family: the plugin itself, same-pluginId libraries, third-party packages, + * and recursively any directly-depended plugin/module (wrapper scenario). + */ +export function filterBundleConfigSchemas( + schemas: { packageName: string }[], + pluginDir: string, +): { packageName: string }[] { + const PLUGIN_OR_MODULE_ROLES = new Set([ + 'backend-plugin', + 'backend-plugin-module', + 'frontend-plugin', + 'frontend-plugin-module', + ]); + const LIBRARY_ROLES = new Set([ + 'node-library', + 'common-library', + 'web-library', + ]); + + const allowed = new Set(); + const visited = new Set(); + const localRequire = createRequire(resolvePath(pluginDir, 'package.json')); + + function walk(pkg: BackstagePackageJson) { + if (visited.has(pkg.name)) { + return; + } + visited.add(pkg.name); + allowed.add(pkg.name); + + const pluginId = pkg.backstage?.pluginId; + + for (const depName of Object.keys(pkg.dependencies ?? {})) { + let depPkgPath: string; + try { + depPkgPath = localRequire.resolve(`${depName}/package.json`); + } catch { + continue; + } + + let depPkg: BackstagePackageJson; + try { + depPkg = fs.readJsonSync(depPkgPath); + } catch { + continue; + } + + const depRole = depPkg.backstage?.role; + + if (!depPkg.backstage) { + allowed.add(depName); + continue; + } + + if (depRole && PLUGIN_OR_MODULE_ROLES.has(depRole)) { + walk(depPkg); + continue; + } + + if ( + depRole && + LIBRARY_ROLES.has(depRole) && + pluginId && + depPkg.backstage?.pluginId === pluginId + ) { + allowed.add(depName); + continue; + } + } + } + + let rootPkg: BackstagePackageJson; + try { + rootPkg = fs.readJsonSync(resolvePath(pluginDir, 'package.json')); + } catch { + return []; + } + walk(rootPkg); + + return schemas.filter(s => allowed.has(s.packageName)); +} + +const ansiPattern = + // eslint-disable-next-line no-control-regex + /[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|\x1b]8;;[^\x07\x1b]*(?:\x07|\x1b\\)/g; +function stripAnsi(str: string): string { + return str.replace(ansiPattern, ''); +} + +function createStepLogger( + logFilePath: string, + verbose: boolean, + prefix?: string, +) { + const logStream = fs.createWriteStream(logFilePath); + + const writeLine = (line: string, stream: 'out' | 'err') => { + const prefixed = prefix ? `${prefix}${line}` : line; + logStream.write( + `${stream === 'err' ? '[WARN] ' : ''}${stripAnsi(prefixed)}\n`, + ); + if (verbose) { + const writer = stream === 'err' ? console.warn : console.log; + writer(chalk.dim(prefixed)); + } + }; + + const logger = { + log(msg: string) { + writeLine(msg, 'out'); + }, + warn(msg: string) { + writeLine(msg, 'err'); + }, + }; + + const logRunOutput = (stream: 'out' | 'err') => (data: Buffer) => { + if (prefix) { + for (const line of data.toString('utf8').split(/\r?\n/)) { + if (line) writeLine(line, stream); + } + } else { + logStream.write( + `${stream === 'err' ? '[WARN] ' : ''}${stripAnsi( + data.toString('utf8'), + )}`, + ); + if (verbose) { + const writer = stream === 'err' ? console.warn : console.log; + writer(chalk.dim(data.toString('utf8'))); + } + } + }; + + const close = () => new Promise(r => logStream.end(r)); + + return { logger, logRunOutput, close, path: logFilePath }; +} + +async function showLogOnError( + logFilePath: string, + verbose: boolean, +): Promise { + console.error( + chalk.red(`\nFull log available at: ${chalk.cyan(logFilePath)}`), + ); + if (!verbose) { + try { + const content = await fs.readFile(logFilePath, 'utf8'); + const tail = content.split('\n').slice(-20).join('\n'); + if (tail) { + console.error(chalk.dim('\n--- last 20 lines ---')); + console.error(tail); + } + } catch { + /* log file may not exist yet */ + } + } +} + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { + outputDestination, + outputName, + clean, + noBuild, + noInstall, + verbose, + prePackedDir, + }, + } = cli( + { + help: info, + flags: { + outputDestination: { + type: String, + description: + 'Directory in which the bundle subdirectory is created. ' + + 'Defaults to the current package directory.', + }, + outputName: { + type: String, + description: + 'Name of the bundle subdirectory. ' + + 'Defaults to "bundle" when output stays in the package directory, ' + + 'or to the mangled package name (e.g. myorg-plugin-foo) when ' + + '--output-destination is specified.', + }, + clean: { + type: Boolean, + description: 'Clean the output directory before bundling', + }, + noBuild: { + type: Boolean, + description: + 'Skip building packages (assumes they are already built)', + }, + noInstall: { + type: Boolean, + description: + 'Skip dependency installation and entrypoint validation.', + }, + verbose: { + type: Boolean, + description: + 'Stream detailed output from internal steps (build, pack, install) to the console. ' + + 'Without this flag, output is captured to per-step log files and only shown on error.', + }, + prePackedDir: { + type: String, + description: + 'Path to a pre-built dist workspace (from build-workspace --alwaysPack). ' + + 'Skips local dependency packing and uses pre-packed packages directly. ' + + 'For frontend plugins, this also enables yarn.lock generation for SBOM.', + }, + }, + }, + undefined, + args, + ); + + return bundleCommand({ + build: !noBuild, + install: !noInstall, + clean: Boolean(clean), + verbose: Boolean(verbose), + outputDestination: outputDestination ?? undefined, + outputName: outputName ?? undefined, + prePackedDir: prePackedDir ?? undefined, + }); +}; diff --git a/packages/cli-module-build/src/commands/package/bundle/index.ts b/packages/cli-module-build/src/commands/package/bundle/index.ts new file mode 100644 index 0000000000..d2d2779ce3 --- /dev/null +++ b/packages/cli-module-build/src/commands/package/bundle/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 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 { default } from './command'; diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli-module-build/src/commands/package/clean.ts similarity index 59% rename from packages/cli/src/modules/build/commands/buildWorkspace.ts rename to packages/cli-module-build/src/commands/package/clean.ts index 159c347eb8..fffc418795 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli-module-build/src/commands/package/clean.ts @@ -14,21 +14,14 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; -import { createDistWorkspace } from '../lib/packager'; +import { targetPaths } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; -type Options = { - alwaysPack?: boolean; -}; - -export default async (dir: string, packages: string[], options: Options) => { - if (!(await fs.pathExists(dir))) { - throw new Error(`Target workspace directory doesn't exist, '${dir}'`); - } - - await createDistWorkspace(packages, { - targetDir: dir, - alwaysPack: options.alwaysPack, - enableFeatureDetection: true, - }); +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); + await fs.remove(targetPaths.resolve('dist')); + await fs.remove(targetPaths.resolve('dist-types')); + await fs.remove(targetPaths.resolve('coverage')); }; diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli-module-build/src/commands/package/postpack.ts similarity index 60% rename from packages/cli/src/modules/config/commands/validate.ts rename to packages/cli-module-build/src/commands/package/postpack.ts index ac0b9d15e0..56856f7401 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli-module-build/src/commands/package/postpack.ts @@ -14,16 +14,12 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; -import { loadCliConfig } from '../lib/config'; +import { cli } from 'cleye'; +import { targetPaths } from '@backstage/cli-common'; +import { revertProductionPack } from '../../lib/packager/productionPack'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async (opts: OptionValues) => { - await loadCliConfig({ - args: opts.config, - fromPackage: opts.package, - mockEnv: opts.lax, - fullVisibility: !opts.frontend, - withDeprecatedKeys: opts.deprecated, - strict: opts.strict, - }); +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); + await revertProductionPack(targetPaths.dir); }; diff --git a/packages/cli/src/modules/maintenance/commands/package/pack.ts b/packages/cli-module-build/src/commands/package/prepack.ts similarity index 61% rename from packages/cli/src/modules/maintenance/commands/package/pack.ts rename to packages/cli-module-build/src/commands/package/prepack.ts index cdd50517ae..4488f92c76 100644 --- a/packages/cli/src/modules/maintenance/commands/package/pack.ts +++ b/packages/cli-module-build/src/commands/package/prepack.ts @@ -14,27 +14,24 @@ * limitations under the License. */ -import { - productionPack, - revertProductionPack, -} from '../../../../modules/build/lib/packager/productionPack'; -import { paths } from '../../../../lib/paths'; +import { cli } from 'cleye'; import fs from 'fs-extra'; +import { targetPaths } from '@backstage/cli-common'; +import { productionPack } from '../../lib/packager/productionPack'; import { publishPreflightCheck } from '../../lib/publishing'; -import { createTypeDistProject } from '../../../../lib/typeDistProject'; +import { createTypeDistProject } from '../../lib/typeDistProject'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); -export const pre = async () => { publishPreflightCheck({ - dir: paths.targetDir, - packageJson: await fs.readJson(paths.resolveTarget('package.json')), + dir: targetPaths.dir, + packageJson: await fs.readJson(targetPaths.resolve('package.json')), }); await productionPack({ - packageDir: paths.targetDir, + packageDir: targetPaths.dir, featureDetectionProject: await createTypeDistProject(), }); }; - -export const post = async () => { - await revertProductionPack(paths.targetDir); -}; diff --git a/packages/cli-module-build/src/commands/package/start/command.ts b/packages/cli-module-build/src/commands/package/start/command.ts new file mode 100644 index 0000000000..8cee25732f --- /dev/null +++ b/packages/cli-module-build/src/commands/package/start/command.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { startPackage } from './startPackage'; +import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; +import { findRoleFromCommand } from '../../../lib/role'; +import { targetPaths } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { + config, + role, + check, + require: requirePath, + link, + entrypoint, + inspect, + inspectBrk, + }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + default: [], + }, + role: { + type: String, + description: 'Run the command with an explicit package role', + }, + check: { + type: Boolean, + description: 'Enable type checking and linting if available', + }, + require: { + type: String, + description: 'Add a --require argument to the node process', + }, + link: { + type: String, + description: 'Link an external workspace for module resolution', + }, + entrypoint: { + type: String, + description: + 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', + }, + inspect: { + type: String, + description: + 'Enable the Node.js inspector, optionally at a specific host:port', + }, + inspectBrk: { + type: String, + description: + 'Enable the Node.js inspector and break before user code starts', + }, + }, + }, + undefined, + args, + ); + + await startPackage({ + role: await findRoleFromCommand({ role }), + entrypoint, + targetDir: targetPaths.dir, + configPaths: config, + checksEnabled: Boolean(check), + linkedWorkspace: await resolveLinkedWorkspace(link), + inspectEnabled: inspect || (inspect === '' ? true : undefined), + inspectBrkEnabled: inspectBrk || (inspectBrk === '' ? true : undefined), + require: requirePath, + }); +}; diff --git a/packages/cli/src/modules/build/commands/package/build/index.ts b/packages/cli-module-build/src/commands/package/start/index.ts similarity index 94% rename from packages/cli/src/modules/build/commands/package/build/index.ts rename to packages/cli-module-build/src/commands/package/start/index.ts index 680fe9e11d..7121eac500 100644 --- a/packages/cli/src/modules/build/commands/package/build/index.ts +++ b/packages/cli-module-build/src/commands/package/start/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { command } from './command'; +export { default } from './command'; diff --git a/packages/cli/src/modules/build/commands/package/start/resolveLinkedWorkspace.ts b/packages/cli-module-build/src/commands/package/start/resolveLinkedWorkspace.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/resolveLinkedWorkspace.ts rename to packages/cli-module-build/src/commands/package/start/resolveLinkedWorkspace.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startBackend.ts b/packages/cli-module-build/src/commands/package/start/startBackend.ts similarity index 94% rename from packages/cli/src/modules/build/commands/package/start/startBackend.ts rename to packages/cli-module-build/src/commands/package/start/startBackend.ts index 525a4b0ba6..a36a93b8ff 100644 --- a/packages/cli/src/modules/build/commands/package/start/startBackend.ts +++ b/packages/cli-module-build/src/commands/package/start/startBackend.ts @@ -16,7 +16,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; -import { paths } from '../../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { runBackend } from '../../../lib/runner'; interface StartBackendOptions { @@ -43,7 +44,7 @@ export async function startBackend(options: StartBackendOptions) { export async function startBackendPlugin(options: StartBackendOptions) { const hasDevIndexEntry = await fs.pathExists( - resolvePath(options.targetDir ?? paths.targetDir, 'dev/index.ts'), + resolvePath(options.targetDir ?? targetPaths.dir, 'dev/index.ts'), ); if (!hasDevIndexEntry) { console.warn( diff --git a/packages/cli/src/modules/build/commands/package/start/startFrontend.ts b/packages/cli-module-build/src/commands/package/start/startFrontend.ts similarity index 87% rename from packages/cli/src/modules/build/commands/package/start/startFrontend.ts rename to packages/cli-module-build/src/commands/package/start/startFrontend.ts index 027dbf68e3..689cd20f7a 100644 --- a/packages/cli/src/modules/build/commands/package/start/startFrontend.ts +++ b/packages/cli-module-build/src/commands/package/start/startFrontend.ts @@ -19,10 +19,11 @@ import { resolve as resolvePath } from 'node:path'; import { getModuleFederationRemoteOptions, serveBundle, -} from '../../../../build/lib/bundler'; -import { paths } from '../../../../../lib/paths'; +} from '../../../lib/bundler'; +import { targetPaths } from '@backstage/cli-common'; + import { BackstagePackageJson } from '@backstage/cli-node'; -import { hasReactDomClient } from '../../../../build/lib/bundler/hasReactDomClient'; +import { hasReactDomClient } from '../../../lib/bundler/hasReactDomClient'; interface StartAppOptions { verifyVersions?: boolean; @@ -38,7 +39,7 @@ interface StartAppOptions { export async function startFrontend(options: StartAppOptions) { const packageJson = (await readJson( - resolvePath(options.targetDir ?? paths.targetDir, 'package.json'), + resolvePath(options.targetDir ?? targetPaths.dir, 'package.json'), )) as BackstagePackageJson; if (!hasReactDomClient()) { @@ -58,7 +59,7 @@ export async function startFrontend(options: StartAppOptions) { moduleFederationRemote: options.isModuleFederationRemote ? await getModuleFederationRemoteOptions( packageJson, - resolvePath(paths.targetDir), + resolvePath(targetPaths.dir), ) : undefined, }); diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli-module-build/src/commands/package/start/startPackage.test.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startPackage.test.ts rename to packages/cli-module-build/src/commands/package/start/startPackage.test.ts diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli-module-build/src/commands/package/start/startPackage.ts similarity index 100% rename from packages/cli/src/modules/build/commands/package/start/startPackage.ts rename to packages/cli-module-build/src/commands/package/start/startPackage.ts diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli-module-build/src/commands/repo/build.ts similarity index 66% rename from packages/cli/src/modules/build/commands/repo/build.ts rename to packages/cli-module-build/src/commands/repo/build.ts index e64fe9eff0..d0bb9cab6c 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli-module-build/src/commands/repo/build.ts @@ -15,31 +15,61 @@ */ import chalk from 'chalk'; -import { Command, OptionValues } from 'commander'; +import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { buildPackages, getOutputsForRole } from '../../lib/builder'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { BackstagePackage, PackageGraph, PackageRoles, + runConcurrentTasks, } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; -import { createScriptOptionsParser } from '../../../../lib/optionsParser'; +import { createScriptOptionsParser } from '../../lib/optionsParser'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { all, since, minify }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + all: { + type: Boolean, + description: + 'Build all packages, including bundled app and backend packages.', + }, + since: { + type: String, + description: + 'Only build packages and their dev dependents that changed since the specified ref', + }, + minify: { + type: Boolean, + description: + 'Minify the generated code. Does not apply to app package (app is minified by default).', + }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); const webpack = process.env.LEGACY_WEBPACK_BUILD ? (require('webpack') as typeof import('webpack')) : undefined; - if (opts.since) { + if (since) { const graph = PackageGraph.fromPackages(packages); const changedPackages = await graph.listChangedPackages({ - ref: opts.since, + ref: since, analyzeLockfile: true, }); const withDevDependents = graph.collectPackageNames( @@ -52,7 +82,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const apps = new Array(); const backends = new Array(); - const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']); + const parseBuildScript = createScriptOptionsParser(['package', 'build'], { + role: { type: 'string' }, + minify: { type: 'boolean' }, + 'skip-build-dependencies': { type: 'boolean' }, + stats: { type: 'boolean' }, + config: { type: 'string', multiple: true }, + 'module-federation': { type: 'boolean' }, + }); const options = packages.flatMap(pkg => { const role = @@ -89,20 +126,20 @@ export async function command(opts: OptionValues, cmd: Command): Promise { targetDir: pkg.dir, packageJson: pkg.packageJson, outputs, - logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, + logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `, workspacePackages: packages, - minify: opts.minify ?? buildOptions.minify, + minify: minify ?? Boolean(buildOptions.minify), }; }); console.log('Building packages'); await buildPackages(options); - if (opts.all) { + if (all) { console.log('Building apps'); - await runParallelWorkers({ + await runConcurrentTasks({ items: apps, - parallelismFactor: 1 / 2, + concurrencyFactor: 1 / 2, worker: async pkg => { const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); if (!buildOptions) { @@ -111,9 +148,12 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); return; } + const configPaths = buildOptions.config; await buildFrontend({ targetDir: pkg.dir, - configPaths: (buildOptions.config as string[]) ?? [], + configPaths: Array.isArray(configPaths) + ? (configPaths as string[]) + : [], writeStats: Boolean(buildOptions.stats), webpack, }); @@ -121,9 +161,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }); console.log('Building backends'); - await runParallelWorkers({ + await runConcurrentTasks({ items: backends, - parallelismFactor: 1 / 2, + concurrencyFactor: 1 / 2, worker: async pkg => { const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); if (!buildOptions) { @@ -135,9 +175,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { await buildBackend({ targetDir: pkg.dir, skipBuildDependencies: true, - minify: opts.minify ?? buildOptions.minify, + minify: minify ?? Boolean(buildOptions.minify), }); }, }); } -} +}; diff --git a/packages/cli/src/modules/maintenance/commands/repo/clean.ts b/packages/cli-module-build/src/commands/repo/clean.ts similarity index 76% rename from packages/cli/src/modules/maintenance/commands/repo/clean.ts rename to packages/cli-module-build/src/commands/repo/clean.ts index 59e5367837..12b1daf911 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/clean.ts +++ b/packages/cli-module-build/src/commands/repo/clean.ts @@ -14,18 +14,20 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; -import { paths } from '../../../../lib/paths'; -import { run } from '@backstage/cli-common'; +import { run, targetPaths } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; -export async function command(): Promise { +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); - await fs.remove(paths.resolveTargetRoot('dist')); - await fs.remove(paths.resolveTargetRoot('dist-types')); - await fs.remove(paths.resolveTargetRoot('coverage')); + await fs.remove(targetPaths.resolveRoot('dist')); + await fs.remove(targetPaths.resolveRoot('dist-types')); + await fs.remove(targetPaths.resolveRoot('coverage')); await Promise.all( Array.from(Array(10), async () => { @@ -48,4 +50,4 @@ export async function command(): Promise { } }), ); -} +}; diff --git a/packages/cli/src/modules/build/commands/repo/start.test.ts b/packages/cli-module-build/src/commands/repo/start.test.ts similarity index 96% rename from packages/cli/src/modules/build/commands/repo/start.test.ts rename to packages/cli-module-build/src/commands/repo/start.test.ts index 0efe01df43..328c8fd365 100644 --- a/packages/cli/src/modules/build/commands/repo/start.test.ts +++ b/packages/cli-module-build/src/commands/repo/start.test.ts @@ -16,8 +16,9 @@ import { PackageGraph } from '@backstage/cli-node'; import { findTargetPackages } from './start'; -import { posix } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; + +overrideTargetPaths('/root'); const mocks = { app: { @@ -97,11 +98,6 @@ const mocks = { describe('findTargetPackages', () => { beforeEach(() => { jest.clearAllMocks(); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...parts: string[]) => { - return posix.resolve('/root', ...parts); - }); }); it('should select default packages', async () => { diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli-module-build/src/commands/repo/start.ts similarity index 79% rename from packages/cli/src/modules/build/commands/repo/start.ts rename to packages/cli-module-build/src/commands/repo/start.ts index bc1ab543c1..8cf3dceb1d 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli-module-build/src/commands/repo/start.ts @@ -20,10 +20,13 @@ import { PackageRole, } from '@backstage/cli-node'; import { relative as relativePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import { cli } from 'cleye'; + import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; import { startPackage } from '../package/start/startPackage'; import { parseArgs } from 'node:util'; +import type { CliCommandContext } from '@backstage/cli-node'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -32,19 +35,62 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ 'backend-plugin', ]; -type CommandOptions = { - plugin: string[]; - config: string[]; - inspect?: boolean | string; - inspectBrk?: boolean | string; - require?: string; - link?: string; -}; +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { plugin, config, require: requirePath, link, inspect, inspectBrk }, + _: namesOrPaths, + } = cli( + { + help: { ...info, usage: `${info.usage} [packages...]` }, + booleanFlagNegation: true, + parameters: ['[packages...]'], + flags: { + plugin: { + type: [String], + description: + 'Start the dev entry-point for any matching plugin package in the repo', + default: [], + }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + default: [], + }, + require: { + type: String, + description: + 'Add a --require argument to the node process. Applies to backend package only', + }, + link: { + type: String, + description: 'Link an external workspace for module resolution', + }, + inspect: { + type: String, + description: + 'Enable the Node.js inspector, optionally at a specific host:port', + }, + inspectBrk: { + type: String, + description: + 'Enable the Node.js inspector and break before user code starts', + }, + }, + }, + undefined, + args, + ); -export async function command(namesOrPaths: string[], options: CommandOptions) { - const targetPackages = await findTargetPackages(namesOrPaths, options.plugin); + const targetPackages = await findTargetPackages(namesOrPaths, plugin); - const packageOptions = await resolvePackageOptions(targetPackages, options); + const packageOptions = await resolvePackageOptions(targetPackages, { + plugin, + config, + inspect: inspect || (inspect === '' ? true : undefined), + inspectBrk: inspectBrk || (inspectBrk === '' ? true : undefined), + require: requirePath, + link, + }); if (packageOptions.length === 0) { console.log('No packages found to start'); @@ -59,7 +105,7 @@ export async function command(namesOrPaths: string[], options: CommandOptions) { // Each of these block until interrupted by user await Promise.all(packageOptions.map(entry => startPackage(entry.options))); -} +}; export async function findTargetPackages( namesOrPaths: string[], @@ -95,7 +141,7 @@ export async function findTargetPackages( pkg => nameOrPath === pkg.packageJson.name, ); if (!matchingPackage) { - const absPath = paths.resolveTargetRoot(nameOrPath); + const absPath = targetPaths.resolveRoot(nameOrPath); matchingPackage = packages.find( pkg => relativePath(pkg.dir, absPath) === '', ); @@ -117,7 +163,7 @@ export async function findTargetPackages( ); if (matchingPackages.length > 1) { // Final fallback is to check for the package path within the monorepo, packages/app or packages/backend - const expectedPath = paths.resolveTargetRoot( + const expectedPath = targetPaths.resolveRoot( role === 'frontend' ? 'packages/app' : 'packages/backend', ); const matchByPath = matchingPackages.find( @@ -164,6 +210,15 @@ export async function findTargetPackages( ); } +type CommandOptions = { + plugin: string[]; + config: string[]; + inspect?: boolean | string; + inspectBrk?: boolean | string; + require?: string; + link?: string; +}; + async function resolvePackageOptions( targetPackages: BackstagePackage[], options: CommandOptions, diff --git a/packages/cli-module-build/src/index.ts b/packages/cli-module-build/src/index.ts new file mode 100644 index 0000000000..cf892db53c --- /dev/null +++ b/packages/cli-module-build/src/index.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2024 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['package', 'build'], + description: 'Build a package for production deployment or publishing', + execute: { loader: () => import('./commands/package/build') }, + }); + + reg.addCommand({ + path: ['repo', 'build'], + description: + 'Build packages in the project, excluding bundled app and backend packages.', + execute: { loader: () => import('./commands/repo/build') }, + }); + + reg.addCommand({ + path: ['package', 'bundle'], + description: + 'Bundle a plugin for dynamic loading. Creates a self-contained plugin ' + + 'package that can be deployed and loaded dynamically by a Backstage application. ' + + 'Supports both backend and frontend plugins. Experimental.', + experimental: true, + execute: { loader: () => import('./commands/package/bundle') }, + }); + + reg.addCommand({ + path: ['package', 'start'], + description: 'Start a package for local development', + execute: { loader: () => import('./commands/package/start') }, + }); + + reg.addCommand({ + path: ['repo', 'start'], + description: 'Starts packages in the repo for local development', + execute: { loader: () => import('./commands/repo/start') }, + }); + + reg.addCommand({ + path: ['package', 'clean'], + description: 'Delete cache directories', + execute: { + loader: () => import('./commands/package/clean'), + }, + }); + + reg.addCommand({ + path: ['package', 'prepack'], + description: 'Prepares a package for packaging before publishing', + execute: { + loader: () => import('./commands/package/prepack'), + }, + }); + + reg.addCommand({ + path: ['package', 'postpack'], + description: 'Restores the changes made by the prepack command', + execute: { + loader: () => import('./commands/package/postpack'), + }, + }); + + reg.addCommand({ + path: ['repo', 'clean'], + description: 'Delete cache and output directories', + execute: { + loader: () => import('./commands/repo/clean'), + }, + }); + + reg.addCommand({ + path: ['build-workspace'], + description: + 'Builds a temporary dist workspace from the provided packages', + execute: { loader: () => import('./commands/buildWorkspace') }, + }); + }, +}); diff --git a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli-module-build/src/lib/__testUtils__/createFeatureEnvironment.ts similarity index 100% rename from packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts rename to packages/cli-module-build/src/lib/__testUtils__/createFeatureEnvironment.ts diff --git a/packages/cli/src/modules/build/lib/buildBackend.ts b/packages/cli-module-build/src/lib/buildBackend.ts similarity index 95% rename from packages/cli/src/modules/build/lib/buildBackend.ts rename to packages/cli-module-build/src/lib/buildBackend.ts index f377d068bd..bf2ebf16ba 100644 --- a/packages/cli/src/modules/build/lib/buildBackend.ts +++ b/packages/cli-module-build/src/lib/buildBackend.ts @@ -19,7 +19,6 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import * as tar from 'tar'; import { createDistWorkspace } from './packager'; -import { getEnvironmentParallelism } from '../../../lib/parallel'; import { buildPackage, Output } from './builder'; import { PackageGraph } from '@backstage/cli-node'; @@ -53,7 +52,6 @@ export async function buildBackend(options: BuildBackendOptions) { configPaths, buildDependencies: !skipBuildDependencies, buildExcludes: [pkg.name], - parallelism: getEnvironmentParallelism(), skeleton: SKELETON_FILE, minify, }); diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli-module-build/src/lib/buildFrontend.ts similarity index 90% rename from packages/cli/src/modules/build/lib/buildFrontend.ts rename to packages/cli-module-build/src/lib/buildFrontend.ts index 7b0955a519..7ca337cfdb 100644 --- a/packages/cli/src/modules/build/lib/buildFrontend.ts +++ b/packages/cli-module-build/src/lib/buildFrontend.ts @@ -17,9 +17,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { buildBundle, getModuleFederationRemoteOptions } from './bundler'; -import { getEnvironmentParallelism } from '../../../lib/parallel'; -import { loadCliConfig } from '../../config/lib/config'; import { BackstagePackageJson } from '@backstage/cli-node'; +import { loadCliConfig } from './config'; interface BuildAppOptions { targetDir: string; @@ -37,7 +36,6 @@ export async function buildFrontend(options: BuildAppOptions) { await buildBundle({ targetDir, entry: 'src/index', - parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, moduleFederationRemote: options.isModuleFederationRemote ? await getModuleFederationRemoteOptions( diff --git a/packages/cli/src/modules/build/lib/builder/config.test.ts b/packages/cli-module-build/src/lib/builder/config.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/config.test.ts rename to packages/cli-module-build/src/lib/builder/config.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/config.ts b/packages/cli-module-build/src/lib/builder/config.ts similarity index 97% rename from packages/cli/src/modules/build/lib/builder/config.ts rename to packages/cli-module-build/src/lib/builder/config.ts index 91ddb68233..4123d2ccf3 100644 --- a/packages/cli/src/modules/build/lib/builder/config.ts +++ b/packages/cli-module-build/src/lib/builder/config.ts @@ -39,9 +39,10 @@ import { import { forwardFileImports, cssEntryPoints } from './plugins'; import { BuildOptions, Output } from './types'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { BackstagePackageJson } from '@backstage/cli-node'; -import { readEntryPoints } from '../../../../lib/entryPoints'; +import { readEntryPoints } from '../entryPoints'; const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; @@ -116,7 +117,7 @@ export async function makeRollupConfigs( options: BuildOptions, ): Promise { const configs = new Array(); - const targetDir = options.targetDir ?? paths.targetDir; + const targetDir = options.targetDir ?? targetPaths.dir; let targetPkg = options.packageJson; if (!targetPkg) { @@ -284,9 +285,9 @@ export async function makeRollupConfigs( const input = Object.fromEntries( scriptEntryPoints.map(e => [ e.name, - paths.resolveTargetRoot( + targetPaths.resolveRoot( 'dist-types', - relativePath(paths.targetRoot, targetDir), + relativePath(targetPaths.rootDir, targetDir), e.path.replace(/\.(?:ts|tsx)$/, '.d.ts'), ), ]), diff --git a/packages/cli/src/modules/build/lib/builder/index.ts b/packages/cli-module-build/src/lib/builder/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/index.ts rename to packages/cli-module-build/src/lib/builder/index.ts diff --git a/packages/cli/src/modules/build/lib/builder/packager.test.ts b/packages/cli-module-build/src/lib/builder/packager.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/packager.test.ts rename to packages/cli-module-build/src/lib/builder/packager.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/packager.ts b/packages/cli-module-build/src/lib/builder/packager.ts similarity index 89% rename from packages/cli/src/modules/build/lib/builder/packager.ts rename to packages/cli-module-build/src/lib/builder/packager.ts index 12017b694c..cee6d0a11c 100644 --- a/packages/cli/src/modules/build/lib/builder/packager.ts +++ b/packages/cli-module-build/src/lib/builder/packager.ts @@ -18,11 +18,11 @@ import fs from 'fs-extra'; import { rollup, RollupOptions } from 'rollup'; import chalk from 'chalk'; import { relative as relativePath, resolve as resolvePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; -import { PackageRoles } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; +import { PackageRoles, runConcurrentTasks } from '@backstage/cli-node'; export function formatErrorMessage(error: any) { let msg = ''; @@ -34,7 +34,7 @@ export function formatErrorMessage(error: any) { msg += `\n\n`; for (const { text, location } of error.errors) { const { line, column } = location; - const path = relativePath(paths.targetDir, error.id); + const path = relativePath(targetPaths.dir, error.id); const loc = chalk.cyan(`${path}:${line}:${column}`); if (text === 'Unexpected "<"' && error.id.endsWith('.js')) { @@ -53,11 +53,11 @@ export function formatErrorMessage(error: any) { } else { // Generic rollup errors, log what's available if (error.loc) { - const file = `${paths.resolveTarget((error.loc.file || error.id)!)}`; + const file = `${targetPaths.resolve((error.loc.file || error.id)!)}`; const pos = `${error.loc.line}:${error.loc.column}`; msg += `${file} [${pos}]\n`; } else if (error.id) { - msg += `${paths.resolveTarget(error.id)}\n`; + msg += `${targetPaths.resolve(error.id)}\n`; } msg += `${error}\n`; @@ -90,7 +90,7 @@ async function rollupBuild(config: RollupOptions) { export const buildPackage = async (options: BuildOptions) => { try { const { resolutions } = await fs.readJson( - paths.resolveTargetRoot('package.json'), + targetPaths.resolveRoot('package.json'), ); if (resolutions?.esbuild) { console.warn( @@ -107,7 +107,7 @@ export const buildPackage = async (options: BuildOptions) => { const rollupConfigs = await makeRollupConfigs(options); - const targetDir = options.targetDir ?? paths.targetDir; + const targetDir = options.targetDir ?? targetPaths.dir; await fs.remove(resolvePath(targetDir, 'dist')); const buildTasks = rollupConfigs.map(rollupBuild); @@ -127,7 +127,7 @@ export const buildPackages = async (options: BuildOptions[]) => { const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts)); - await runParallelWorkers({ + await runConcurrentTasks({ items: buildTasks, worker: async task => task(), }); diff --git a/packages/cli/src/modules/build/lib/builder/plugins.test.ts b/packages/cli-module-build/src/lib/builder/plugins.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/plugins.test.ts rename to packages/cli-module-build/src/lib/builder/plugins.test.ts diff --git a/packages/cli/src/modules/build/lib/builder/plugins.ts b/packages/cli-module-build/src/lib/builder/plugins.ts similarity index 99% rename from packages/cli/src/modules/build/lib/builder/plugins.ts rename to packages/cli-module-build/src/lib/builder/plugins.ts index 2340884e3d..93b657bd09 100644 --- a/packages/cli/src/modules/build/lib/builder/plugins.ts +++ b/packages/cli-module-build/src/lib/builder/plugins.ts @@ -29,7 +29,7 @@ import { OutputChunk, HasModuleSideEffects, } from 'rollup'; -import { EntryPoint } from '../../../../lib/entryPoints'; +import { EntryPoint } from '../entryPoints'; type ForwardFileImportsOptions = { include: Array | string | RegExp | null; diff --git a/packages/cli/src/modules/build/lib/builder/types.ts b/packages/cli-module-build/src/lib/builder/types.ts similarity index 100% rename from packages/cli/src/modules/build/lib/builder/types.ts rename to packages/cli-module-build/src/lib/builder/types.ts diff --git a/packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts b/packages/cli-module-build/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts rename to packages/cli-module-build/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts diff --git a/packages/cli/src/modules/build/lib/bundler/bundle.ts b/packages/cli-module-build/src/lib/bundler/bundle.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/bundle.ts rename to packages/cli-module-build/src/lib/bundler/bundle.ts diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli-module-build/src/lib/bundler/config.ts similarity index 97% rename from packages/cli/src/modules/build/lib/bundler/config.ts rename to packages/cli-module-build/src/lib/bundler/config.ts index a4e2b16ab9..cee7ba6819 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli-module-build/src/lib/bundler/config.ts @@ -25,13 +25,14 @@ import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; -import { paths as cliPaths } from '../../../../lib/paths'; + import fs from 'fs-extra'; import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; -import { runOutput } from '@backstage/cli-common'; +import { findOwnPaths, runOutput, targetPaths } from '@backstage/cli-common'; + import { transforms } from './transforms'; -import { version } from '../../../../lib/version'; +import { version } from '../../../package.json'; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; @@ -96,7 +97,7 @@ async function readBuildInfo() { } const { version: packageVersion } = await fs.readJson( - cliPaths.resolveTarget('package.json'), + targetPaths.resolve('package.json'), ); return { @@ -329,7 +330,8 @@ export async function createConfig( devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, entry: [ - require.resolve('@backstage/cli/config/webpack-public-path'), + /* eslint-disable-next-line no-restricted-syntax */ + findOwnPaths(__dirname).resolve('config/webpack-public-path'), ...(options.additionalEntryPoints ?? []), paths.targetEntry, ], diff --git a/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts b/packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts similarity index 89% rename from packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts rename to packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts index ad331bdf8b..9dfc3fef5f 100644 --- a/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts +++ b/packages/cli-module-build/src/lib/bundler/hasReactDomClient.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; export function hasReactDomClient() { try { require.resolve('react-dom/client', { - paths: [paths.targetDir], + paths: [targetPaths.dir], }); return true; } catch { diff --git a/packages/cli/src/modules/build/lib/bundler/index.ts b/packages/cli-module-build/src/lib/bundler/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/index.ts rename to packages/cli-module-build/src/lib/bundler/index.ts diff --git a/packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts b/packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts similarity index 95% rename from packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts rename to packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts index 39f52067ab..f724e444b4 100644 --- a/packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts +++ b/packages/cli-module-build/src/lib/bundler/linkWorkspaces.ts @@ -17,7 +17,7 @@ import { relative as relativePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { rspack } from '@rspack/core'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; /** * This returns of collection of plugins that links a separate workspace into @@ -52,7 +52,7 @@ export async function createWorkspaceLinkingPlugins( /^react(?:-router)?(?:-dom)?$/, resource => { if (!relativePath(linkedRoot.dir, resource.context).startsWith('..')) { - resource.context = paths.targetDir; + resource.context = targetPaths.dir; } }, ), diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts b/packages/cli-module-build/src/lib/bundler/moduleFederation.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/moduleFederation.test.ts rename to packages/cli-module-build/src/lib/bundler/moduleFederation.test.ts diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts b/packages/cli-module-build/src/lib/bundler/moduleFederation.ts similarity index 98% rename from packages/cli/src/modules/build/lib/bundler/moduleFederation.ts rename to packages/cli-module-build/src/lib/bundler/moduleFederation.ts index 8faab7f732..69e1415b5d 100644 --- a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts +++ b/packages/cli-module-build/src/lib/bundler/moduleFederation.ts @@ -16,11 +16,11 @@ import { ModuleFederationRemoteOptions } from './types'; import { BackstagePackageJson } from '@backstage/cli-node'; -import { readEntryPoints } from '../../../../lib/entryPoints'; +import { readEntryPoints } from '../entryPoints'; import { createTypeDistProject, getEntryPointDefaultFeatureType, -} from '../../../../lib/typeDistProject'; +} from '../typeDistProject'; import { BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, defaultRemoteSharedDependencies, @@ -28,7 +28,7 @@ import { HostSharedDependencies, RuntimeSharedDependenciesGlobal, } from '@backstage/module-federation-common'; -import { dirname, join as joinPath, resolve as resolvePath } from 'path'; +import { dirname, join as joinPath, resolve as resolvePath } from 'node:path'; import fs from 'fs-extra'; import chokidar from 'chokidar'; import PQueue from 'p-queue'; diff --git a/packages/cli/src/modules/build/lib/bundler/optimization.ts b/packages/cli-module-build/src/lib/bundler/optimization.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/optimization.ts rename to packages/cli-module-build/src/lib/bundler/optimization.ts diff --git a/packages/cli/src/modules/build/lib/bundler/packageDetection.ts b/packages/cli-module-build/src/lib/bundler/packageDetection.ts similarity index 98% rename from packages/cli/src/modules/build/lib/bundler/packageDetection.ts rename to packages/cli-module-build/src/lib/bundler/packageDetection.ts index 47346f803b..5b4837a133 100644 --- a/packages/cli/src/modules/build/lib/bundler/packageDetection.ts +++ b/packages/cli-module-build/src/lib/bundler/packageDetection.ts @@ -20,7 +20,7 @@ import chokidar from 'chokidar'; import fs from 'fs-extra'; import PQueue from 'p-queue'; import { dirname, join as joinPath, resolve as resolvePath } from 'node:path'; -import { paths as cliPaths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; const DETECTED_MODULES_MODULE_NAME = '__backstage-autodetected-plugins__'; @@ -146,7 +146,7 @@ export async function createDetectedModulesEntryPoint(options: { // Previous versions of the CLI would write the detected modules file to the // root `node_modules`, this makes sure that doesn't exist to minimize risk of conflicts const legacyDetectedModulesPath = joinPath( - cliPaths.targetRoot, + targetPaths.rootDir, 'node_modules', `${DETECTED_MODULES_MODULE_NAME}.js`, ); diff --git a/packages/cli/src/modules/build/lib/bundler/paths.ts b/packages/cli-module-build/src/lib/bundler/paths.ts similarity index 83% rename from packages/cli/src/modules/build/lib/bundler/paths.ts rename to packages/cli-module-build/src/lib/bundler/paths.ts index 3106da5f7d..5a7d3b14dc 100644 --- a/packages/cli/src/modules/build/lib/bundler/paths.ts +++ b/packages/cli-module-build/src/lib/bundler/paths.ts @@ -16,19 +16,19 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths, findOwnPaths } from '@backstage/cli-common'; export type BundlingPathsOptions = { // bundle entrypoint, e.g. 'src/index' entry: string; - // Target directory, defaulting to paths.targetDir + // Target directory, defaulting to targetPaths.dir targetDir?: string; // Relative dist directory, defaulting to 'dist' dist?: string; }; export function resolveBundlingPaths(options: BundlingPathsOptions) { - const { entry, targetDir = paths.targetDir } = options; + const { entry, targetDir = targetPaths.dir } = options; const resolveTargetModule = (pathString: string) => { for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) { @@ -49,7 +49,10 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { } else { targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { - targetHtml = paths.resolveOwn('templates/serve_index.html'); + /* eslint-disable-next-line no-restricted-syntax */ + targetHtml = findOwnPaths(__dirname).resolve( + 'templates/serve_index.html', + ); } } @@ -67,10 +70,10 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { targetSrc: resolvePath(targetDir, 'src'), targetDev: resolvePath(targetDir, 'dev'), targetEntry: resolveTargetModule(entry), - targetTsConfig: paths.resolveTargetRoot('tsconfig.json'), + targetTsConfig: targetPaths.resolveRoot('tsconfig.json'), targetPackageJson: resolvePath(targetDir, 'package.json'), - rootNodeModules: paths.resolveTargetRoot('node_modules'), - root: paths.targetRoot, + rootNodeModules: targetPaths.resolveRoot('node_modules'), + root: targetPaths.rootDir, }; } diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli-module-build/src/lib/bundler/server.ts similarity index 97% rename from packages/cli/src/modules/build/lib/bundler/server.ts rename to packages/cli-module-build/src/lib/bundler/server.ts index b2d480f061..e474157c7c 100644 --- a/packages/cli/src/modules/build/lib/bundler/server.ts +++ b/packages/cli-module-build/src/lib/bundler/server.ts @@ -22,8 +22,9 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { rspack } from '@rspack/core'; import { RspackDevServer } from '@rspack/dev-server'; -import { paths as libPaths } from '../../../../lib/paths'; -import { loadCliConfig } from '../../../config/lib/config'; +import { targetPaths } from '@backstage/cli-common'; + +import { loadCliConfig } from '../config'; import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; @@ -53,7 +54,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be checkReactVersion(); const { name } = await fs.readJson( - resolvePath(options.targetDir ?? libPaths.targetDir, 'package.json'), + resolvePath(options.targetDir ?? targetPaths.dir, 'package.json'), ); let devServer: RspackDevServer | undefined = undefined; @@ -271,7 +272,7 @@ function checkReactVersion() { try { // Make sure we're looking at the root of the target repo const reactPkgPath = require.resolve('react/package.json', { - paths: [libPaths.targetRoot], + paths: [targetPaths.rootDir], }); const reactPkg = require(reactPkgPath); if (reactPkg.version.startsWith('16.')) { diff --git a/packages/cli/src/modules/build/lib/bundler/transforms.ts b/packages/cli-module-build/src/lib/bundler/transforms.ts similarity index 100% rename from packages/cli/src/modules/build/lib/bundler/transforms.ts rename to packages/cli-module-build/src/lib/bundler/transforms.ts diff --git a/packages/cli/src/modules/build/lib/bundler/types.ts b/packages/cli-module-build/src/lib/bundler/types.ts similarity index 97% rename from packages/cli/src/modules/build/lib/bundler/types.ts rename to packages/cli-module-build/src/lib/bundler/types.ts index a9a4b0f51f..d8960bc03e 100644 --- a/packages/cli/src/modules/build/lib/bundler/types.ts +++ b/packages/cli-module-build/src/lib/bundler/types.ts @@ -36,7 +36,6 @@ export type BundlingOptions = { isDev: boolean; frontendConfig: Config; getFrontendAppConfigs(): AppConfig[]; - parallelism?: number; additionalEntryPoints?: string[]; // Path to append to the detected public path, e.g. '/public' publicSubPath?: string; @@ -63,7 +62,6 @@ export type BuildOptions = BundlingPathsOptions & { // Target directory, defaulting to paths.targetDir targetDir?: string; statsJsonEnabled: boolean; - parallelism?: number; schema?: ConfigSchema; frontendConfig: Config; frontendAppConfigs: AppConfig[]; @@ -75,7 +73,6 @@ export type BuildOptions = BundlingPathsOptions & { export type BackendBundlingOptions = { checksEnabled: boolean; isDev: boolean; - parallelism?: number; inspectEnabled: boolean; inspectBrkEnabled: boolean; require?: string; diff --git a/packages/cli-module-build/src/lib/config.ts b/packages/cli-module-build/src/lib/config.ts new file mode 100644 index 0000000000..56bfc68ced --- /dev/null +++ b/packages/cli-module-build/src/lib/config.ts @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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 { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; +import { AppConfig, ConfigReader } from '@backstage/config'; +import { targetPaths } from '@backstage/cli-common'; + +import { getPackages } from '@manypkg/get-packages'; +import { PackageGraph } from '@backstage/cli-node'; +import { resolve as resolvePath } from 'node:path'; + +type Options = { + args: string[]; + targetDir?: string; + fromPackage?: string; + withFilteredKeys?: boolean; + watch?: (newFrontendAppConfigs: AppConfig[]) => void; +}; + +export async function loadCliConfig(options: Options) { + const targetDir = options.targetDir ?? targetPaths.dir; + + const { packages } = await getPackages(targetDir); + + let localPackageNames; + if (options.fromPackage) { + if (packages.length) { + const graph = PackageGraph.fromPackages(packages); + localPackageNames = Array.from( + graph.collectPackageNames([options.fromPackage], node => { + // Workaround for Backstage main repo only, since the CLI has some artificial devDependencies + if (node.name === '@backstage/cli') { + return undefined; + } + return node.localDependencies.keys(); + }), + ); + } else { + localPackageNames = [options.fromPackage]; + } + } else { + localPackageNames = packages.map(p => p.packageJson.name); + } + + const schema = await loadConfigSchema({ + dependencies: localPackageNames, + packagePaths: [targetPaths.resolveRoot('package.json')], + }); + + const source = ConfigSources.default({ + allowMissingDefaultConfig: true, + watch: Boolean(options.watch), + rootDir: targetPaths.rootDir, + argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]), + }); + + const appConfigs = await new Promise((resolve, reject) => { + async function loadConfigReaderLoop() { + let loaded = false; + + try { + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + if (loaded) { + const newFrontendAppConfigs = schema.process(configs, { + visibility: ['frontend'], + withFilteredKeys: options.withFilteredKeys, + ignoreSchemaErrors: true, + }); + options.watch?.(newFrontendAppConfigs); + } else { + resolve(configs); + loaded = true; + + if (!options.watch) { + abortController.abort(); + } + } + } + } catch (error) { + if (loaded) { + console.error(`Failed to reload configuration, ${error}`); + } else { + reject(error); + } + } + } + loadConfigReaderLoop(); + }); + + const configurationLoadedMessage = appConfigs.length + ? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}` + : `No configuration files found, running without config`; + + process.stderr.write(`${configurationLoadedMessage}\n`); + + const frontendAppConfigs = schema.process(appConfigs, { + visibility: ['frontend'], + withFilteredKeys: options.withFilteredKeys, + ignoreSchemaErrors: true, + }); + const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); + + const fullConfig = ConfigReader.fromConfigs(appConfigs); + + return { + schema, + appConfigs, + frontendConfig, + frontendAppConfigs, + fullConfig, + }; +} diff --git a/packages/cli/src/lib/entryPoints.ts b/packages/cli-module-build/src/lib/entryPoints.ts similarity index 100% rename from packages/cli/src/lib/entryPoints.ts rename to packages/cli-module-build/src/lib/entryPoints.ts diff --git a/packages/cli/src/modules/build/lib/ipc/IpcServer.ts b/packages/cli-module-build/src/lib/ipc/IpcServer.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/IpcServer.ts rename to packages/cli-module-build/src/lib/ipc/IpcServer.ts diff --git a/packages/cli/src/modules/build/lib/ipc/ServerDataStore.ts b/packages/cli-module-build/src/lib/ipc/ServerDataStore.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/ServerDataStore.ts rename to packages/cli-module-build/src/lib/ipc/ServerDataStore.ts diff --git a/packages/cli/src/modules/build/lib/ipc/index.ts b/packages/cli-module-build/src/lib/ipc/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/ipc/index.ts rename to packages/cli-module-build/src/lib/ipc/index.ts diff --git a/packages/cli-module-build/src/lib/optionsParser.ts b/packages/cli-module-build/src/lib/optionsParser.ts new file mode 100644 index 0000000000..62c3b7ce71 --- /dev/null +++ b/packages/cli-module-build/src/lib/optionsParser.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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 { parseArgs, type ParseArgsConfig } from 'node:util'; +import { parse as parseShellArgs } from 'shell-quote'; + +export function createScriptOptionsParser( + commandPath: string[], + options: ParseArgsConfig['options'], +) { + const expectedScript = `backstage-cli ${commandPath.join(' ')}`; + + return (scriptStr?: string) => { + if (!scriptStr || !scriptStr.startsWith(expectedScript)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedScript.length).trim(); + const args = argsStr + ? parseShellArgs(argsStr).filter( + (e): e is string => typeof e === 'string', + ) + : []; + + const { values } = parseArgs({ args, strict: false, options }); + return values; + }; +} diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts similarity index 73% rename from packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts rename to packages/cli-module-build/src/lib/packager/createDistWorkspace.ts index 0f096a3127..72a06af69f 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli-module-build/src/lib/packager/createDistWorkspace.ts @@ -24,12 +24,12 @@ import { import { tmpdir } from 'node:os'; import * as tar from 'tar'; import partition from 'lodash/partition'; -import { paths } from '../../../../lib/paths'; -import { run } from '@backstage/cli-common'; + +import { run, targetPaths } from '@backstage/cli-common'; import { dependencies as cliDependencies, devDependencies as cliDevDependencies, -} from '../../../../../package.json'; +} from '../../../package.json'; import { BuildOptions, buildPackages, @@ -38,12 +38,13 @@ import { } from '../builder'; import { productionPack } from './productionPack'; import { + BackstagePackage, PackageRoles, PackageGraph, PackageGraphNode, + runConcurrentTasks, } from '@backstage/cli-node'; -import { runParallelWorkers } from '../../../../lib/parallel'; -import { createTypeDistProject } from '../../../../lib/typeDistProject'; +import { createTypeDistProject } from '../typeDistProject'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ @@ -86,11 +87,6 @@ type Options = { */ buildExcludes?: string[]; - /** - * Controls amount of parallelism in some build steps. - */ - parallelism?: number; - /** * If set, creates a skeleton tarball that contains all package.json files * with the same structure as the workspace dir. @@ -114,12 +110,21 @@ type Options = { * If set to true, the generated code will be minified. */ minify?: boolean; + + /** + * Optional logger to route console output through. When not provided, + * output goes to `console.log` / `console.warn` as before. + */ + logger?: { + log(msg: string): void; + warn(msg: string): void; + }; }; -function prefixLogFunc(prefix: string, out: 'stdout' | 'stderr') { +function prefixLogFunc(prefix: string, logFn: (msg: string) => void) { return (data: Buffer) => { for (const line of data.toString('utf8').split(/\r?\n/)) { - process[out].write(`${prefix} ${line}\n`); + if (line) logFn(`${prefix}${line}`); } }; } @@ -136,21 +141,14 @@ export async function createDistWorkspace( packageNames: string[], options: Options = {}, ) { + const logger = options.logger ?? { log: console.log, warn: console.warn }; + const targetDir = options.targetDir ?? (await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace'))); const packages = await PackageGraph.listTargetPackages(); - const packageGraph = PackageGraph.fromPackages(packages); - const targetNames = packageGraph.collectPackageNames(packageNames, node => { - // Don't include dependencies of packages that are marked as bundled - if (node.packageJson.bundled) { - return undefined; - } - - return node.publishedLocalDependencies.keys(); - }); - const targets = Array.from(targetNames).map(name => packageGraph.get(name)!); + const targets = await resolveLocalDependencies(packageNames, packages); if (options.buildDependencies) { const exclude = options.buildExcludes ?? []; @@ -173,7 +171,7 @@ export async function createDistWorkspace( } const role = pkg.packageJson.backstage?.role; if (!role) { - console.warn( + logger.warn( `Building ${pkg.packageJson.name} separately because it has no role`, ); customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); @@ -187,7 +185,7 @@ export async function createDistWorkspace( } if (!buildScript.startsWith('backstage-cli package build')) { - console.warn( + logger.warn( `Building ${pkg.packageJson.name} separately because it has a custom build script, '${buildScript}'`, ); customBuild.push({ dir: pkg.dir, name: pkg.packageJson.name }); @@ -195,7 +193,7 @@ export async function createDistWorkspace( } if (PackageRoles.getRoleInfo(role).output.includes('bundle')) { - console.warn( + logger.warn( `Building ${pkg.packageJson.name} separately because it is a bundled package`, ); const args = buildScript.includes('--config') @@ -215,7 +213,9 @@ export async function createDistWorkspace( targetDir: pkg.dir, packageJson: pkg.packageJson, outputs: outputs, - logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, + logPrefix: `${chalk.cyan( + relativePath(targetPaths.rootDir, pkg.dir), + )}: `, minify: options.minify, workspacePackages: packages, }); @@ -225,13 +225,13 @@ export async function createDistWorkspace( await buildPackages(standardBuilds); if (customBuild.length > 0) { - await runParallelWorkers({ + await runConcurrentTasks({ items: customBuild, worker: async ({ name, dir, args }) => { await run(['yarn', 'run', 'build', ...(args || [])], { cwd: dir, - onStdout: prefixLogFunc(`${name}: `, 'stdout'), - onStderr: prefixLogFunc(`${name}: `, 'stderr'), + onStdout: prefixLogFunc(`${name}: `, logger.log), + onStderr: prefixLogFunc(`${name}: `, logger.warn), }).waitForExit(); }, }); @@ -243,6 +243,7 @@ export async function createDistWorkspace( targets, Boolean(options.alwaysPack), Boolean(options.enableFeatureDetection), + logger, ); const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json']; @@ -250,13 +251,13 @@ export async function createDistWorkspace( for (const file of files) { const src = typeof file === 'string' ? file : file.src; const dest = typeof file === 'string' ? file : file.dest; - await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest)); + await fs.copy(targetPaths.resolveRoot(src), resolvePath(targetDir, dest)); } if (options.skeleton) { const skeletonFiles = targets .map(target => { - const dir = relativePath(paths.targetRoot, target.dir); + const dir = relativePath(targetPaths.rootDir, target.dir); return joinPath(dir, 'package.json'); }) .sort(); @@ -273,7 +274,7 @@ export async function createDistWorkspace( ); } - return targetDir; + return { targetDir, targets }; } const FAST_PACK_SCRIPTS = [ @@ -282,11 +283,40 @@ const FAST_PACK_SCRIPTS = [ 'backstage-cli package prepack', ]; +/** + * Runs `yarn pack` on a single package and extracts the resulting tarball + * into `targetDir`. This resolves `workspace:^` and `backstage:^` dependency + * specs to concrete versions via yarn's `beforeWorkspacePacking` hook. + */ +export async function packToDirectory(options: { + packageDir: string; + packageName: string; + targetDir: string; + archivePath?: string; + logger: { log(msg: string): void; warn(msg: string): void }; +}): Promise { + const { packageDir, packageName, targetDir, logger } = options; + const archivePath = + options.archivePath ?? resolvePath(targetDir, 'temp-archive.tgz'); + const prefix = `${packageName} [pack]: `; + + await fs.ensureDir(targetDir); + await run(['yarn', 'pack', '--filename', archivePath], { + cwd: packageDir, + onStdout: prefixLogFunc(prefix, logger.log), + onStderr: prefixLogFunc(prefix, logger.warn), + }).waitForExit(); + + await tar.extract({ file: archivePath, cwd: targetDir, strip: 1 }); + await fs.remove(archivePath); +} + async function moveToDistWorkspace( workspaceDir: string, localPackages: PackageGraphNode[], alwaysPack: boolean, enableFeatureDetection: boolean, + logger: { log(msg: string): void; warn(msg: string): void }, ): Promise { const [fastPackPackages, slowPackPackages] = partition( localPackages, @@ -303,9 +333,9 @@ async function moveToDistWorkspace( // New an improved flow where we avoid calling `yarn pack` await Promise.all( fastPackPackages.map(async target => { - console.log(`Moving ${target.name} into dist workspace`); + logger.log(`Moving ${target.name} into dist workspace`); - const outputDir = relativePath(paths.targetRoot, target.dir); + const outputDir = relativePath(targetPaths.rootDir, target.dir); const absoluteOutputPath = resolvePath(workspaceDir, outputDir); await productionPack({ packageDir: target.dir, @@ -318,23 +348,18 @@ async function moveToDistWorkspace( // Old flow is below, which calls `yarn pack` and extracts the tarball async function pack(target: PackageGraphNode, archive: string) { - console.log(`Repacking ${target.name} into dist workspace`); - const archivePath = resolvePath(workspaceDir, archive); - - await run(['yarn', 'pack', '--filename', archivePath], { - cwd: target.dir, - }).waitForExit(); - - const outputDir = relativePath(paths.targetRoot, target.dir); - const absoluteOutputPath = resolvePath(workspaceDir, outputDir); - await fs.ensureDir(absoluteOutputPath); - - await tar.extract({ - file: archivePath, - cwd: absoluteOutputPath, - strip: 1, + logger.log(`Repacking ${target.name} into dist workspace`); + const absoluteOutputPath = resolvePath( + workspaceDir, + relativePath(targetPaths.rootDir, target.dir), + ); + await packToDirectory({ + packageDir: target.dir, + packageName: target.name, + targetDir: absoluteOutputPath, + archivePath: resolvePath(workspaceDir, archive), + logger, }); - await fs.remove(archivePath); // We remove the dependencies from package.json of packages that are marked // as bundled, so that yarn doesn't try to install them. @@ -368,10 +393,35 @@ async function moveToDistWorkspace( } // Repacking in parallel is much faster and safe for all packages outside of the Backstage repo - await runParallelWorkers({ + await runConcurrentTasks({ items: safePackages.map((target, index) => ({ target, index })), worker: async ({ target, index }) => { await pack(target, `temp-package-${index}.tgz`); }, }); } + +/** + * Resolves the full set of local (workspace) packages that the given + * package names transitively depend on via `publishedLocalDependencies`. + * Packages marked as `bundled` have their own dependencies excluded. + * + * This is the same traversal that `createDistWorkspace` performs internally. + * Callers must supply the monorepo package list obtained from + * `PackageGraph.listTargetPackages()`. + */ +export async function resolveLocalDependencies( + packageNames: string[], + packages: BackstagePackage[], +): Promise { + const packageGraph = PackageGraph.fromPackages(packages); + const targetNames = packageGraph.collectPackageNames(packageNames, node => { + // Don't include dependencies of packages that are marked as bundled + if (node.packageJson.bundled) { + return undefined; + } + + return node.publishedLocalDependencies.keys(); + }); + return Array.from(targetNames).map(name => packageGraph.get(name)!); +} diff --git a/packages/cli-module-build/src/lib/packager/index.ts b/packages/cli-module-build/src/lib/packager/index.ts new file mode 100644 index 0000000000..4b382d0340 --- /dev/null +++ b/packages/cli-module-build/src/lib/packager/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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 { + createDistWorkspace, + packToDirectory, + resolveLocalDependencies, +} from './createDistWorkspace'; diff --git a/packages/cli/src/modules/build/lib/packager/productionPack.ts b/packages/cli-module-build/src/lib/packager/productionPack.ts similarity index 98% rename from packages/cli/src/modules/build/lib/packager/productionPack.ts rename to packages/cli-module-build/src/lib/packager/productionPack.ts index f2a74179f4..55eb6edc48 100644 --- a/packages/cli/src/modules/build/lib/packager/productionPack.ts +++ b/packages/cli-module-build/src/lib/packager/productionPack.ts @@ -18,8 +18,8 @@ import fs from 'fs-extra'; import npmPackList from 'npm-packlist'; import { resolve as resolvePath, posix as posixPath } from 'node:path'; import { BackstagePackageJson } from '@backstage/cli-node'; -import { readEntryPoints } from '../../../../lib/entryPoints'; -import { getEntryPointDefaultFeatureType } from '../../../../lib/typeDistProject'; +import { readEntryPoints } from '../entryPoints'; +import { getEntryPointDefaultFeatureType } from '../typeDistProject'; import { Project } from 'ts-morph'; const PKG_PATH = 'package.json'; diff --git a/packages/cli/src/modules/maintenance/lib/publishing.ts b/packages/cli-module-build/src/lib/publishing.ts similarity index 100% rename from packages/cli/src/modules/maintenance/lib/publishing.ts rename to packages/cli-module-build/src/lib/publishing.ts diff --git a/packages/cli/src/lib/role.test.ts b/packages/cli-module-build/src/lib/role.test.ts similarity index 62% rename from packages/cli/src/lib/role.test.ts rename to packages/cli-module-build/src/lib/role.test.ts index 8163512b76..b046d42432 100644 --- a/packages/cli/src/lib/role.test.ts +++ b/packages/cli-module-build/src/lib/role.test.ts @@ -15,27 +15,13 @@ */ import { createMockDirectory } from '@backstage/backend-test-utils'; -import { Command } from 'commander'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import { findRoleFromCommand } from './role'; const mockDir = createMockDirectory(); - -jest.mock('./paths', () => ({ - paths: { - resolveTarget(filename: string) { - return mockDir.resolve(filename); - }, - }, -})); +overrideTargetPaths(mockDir.path); describe('findRoleFromCommand', () => { - function mkCommand(args: string) { - const parsed = new Command() - .option('--role ', 'test role') - .parse(['node', 'entry.js', ...args.split(' ')]) as Command; - return parsed.opts(); - } - beforeEach(() => { mockDir.setContent({ 'package.json': JSON.stringify({ @@ -48,16 +34,14 @@ describe('findRoleFromCommand', () => { }); it('provides role info by role', async () => { - await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( - 'web-library', - ); + await expect(findRoleFromCommand({})).resolves.toEqual('web-library'); await expect( - findRoleFromCommand(mkCommand('--role node-library')), + findRoleFromCommand({ role: 'node-library' }), ).resolves.toEqual('node-library'); - await expect( - findRoleFromCommand(mkCommand('--role invalid')), - ).rejects.toThrow(`Unknown package role 'invalid'`); + await expect(findRoleFromCommand({ role: 'invalid' })).rejects.toThrow( + `Unknown package role 'invalid'`, + ); }); }); diff --git a/packages/cli/src/lib/role.ts b/packages/cli-module-build/src/lib/role.ts similarity index 75% rename from packages/cli/src/lib/role.ts rename to packages/cli-module-build/src/lib/role.ts index 49f39f789c..26c9da7cd0 100644 --- a/packages/cli/src/lib/role.ts +++ b/packages/cli-module-build/src/lib/role.ts @@ -15,18 +15,18 @@ */ import fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { paths } from './paths'; +import { targetPaths } from '@backstage/cli-common'; + import { PackageRoles, PackageRole } from '@backstage/cli-node'; -export async function findRoleFromCommand( - opts: OptionValues, -): Promise { +export async function findRoleFromCommand(opts: { + role?: string; +}): Promise { if (opts.role) { - return PackageRoles.getRoleInfo(opts.role)?.role; + return PackageRoles.getRoleInfo(opts.role).role; } - const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const pkg = await fs.readJson(targetPaths.resolve('package.json')); const info = PackageRoles.getRoleFromPackage(pkg); if (!info) { throw new Error(`Target package must have 'backstage.role' set`); diff --git a/packages/cli/src/modules/build/lib/runner/index.ts b/packages/cli-module-build/src/lib/runner/index.ts similarity index 100% rename from packages/cli/src/modules/build/lib/runner/index.ts rename to packages/cli-module-build/src/lib/runner/index.ts diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/runner/runBackend.test.ts rename to packages/cli-module-build/src/lib/runner/runBackend.test.ts diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts similarity index 96% rename from packages/cli/src/modules/build/lib/runner/runBackend.ts rename to packages/cli-module-build/src/lib/runner/runBackend.ts index d8fd447326..86c249e8be 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -21,14 +21,15 @@ import { IpcServer, ServerDataStore } from '../ipc'; import debounce from 'lodash/debounce'; import { fileURLToPath } from 'node:url'; import { isAbsolute as isAbsolutePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import spawn from 'cross-spawn'; const loaderArgs = [ '--enable-source-maps', '--require', - require.resolve('@backstage/cli/config/nodeTransform.cjs'), - // TODO: Support modules, although there's currently no way to load them since import() is transpiled tp require() + require.resolve('@backstage/cli-node/config/nodeTransform.cjs'), + // TODO: Support modules, although there's currently no way to load them since import() is transpiled to require() ]; export type RunBackendOptions = { @@ -135,7 +136,7 @@ export async function runBackend(options: RunBackendOptions) { ...process.env, BACKSTAGE_CLI_LINKED_WORKSPACE: options.linkedWorkspace, BACKSTAGE_CLI_CHANNEL: '1', - ESBK_TSCONFIG_PATH: paths.resolveTargetRoot('tsconfig.json'), + ESBK_TSCONFIG_PATH: targetPaths.resolveRoot('tsconfig.json'), }, serialization: 'advanced', }, diff --git a/packages/cli/src/lib/typeDistProject.test.ts b/packages/cli-module-build/src/lib/typeDistProject.test.ts similarity index 99% rename from packages/cli/src/lib/typeDistProject.test.ts rename to packages/cli-module-build/src/lib/typeDistProject.test.ts index 6eef53816b..9d14bccc15 100644 --- a/packages/cli/src/lib/typeDistProject.test.ts +++ b/packages/cli-module-build/src/lib/typeDistProject.test.ts @@ -33,6 +33,7 @@ describe('typeDistProject', () => { frontend: false, backend: false, cli: false, + 'cli-module': false, 'common-library': false, }; diff --git a/packages/cli/src/lib/typeDistProject.ts b/packages/cli-module-build/src/lib/typeDistProject.ts similarity index 97% rename from packages/cli/src/lib/typeDistProject.ts rename to packages/cli-module-build/src/lib/typeDistProject.ts index d8469aee37..e9cc96d988 100644 --- a/packages/cli/src/lib/typeDistProject.ts +++ b/packages/cli-module-build/src/lib/typeDistProject.ts @@ -20,11 +20,11 @@ import { } from '@backstage/cli-node'; import { resolve as resolvePath } from 'node:path'; import { Project, SourceFile, SyntaxKind, ts, Type } from 'ts-morph'; -import { paths } from './paths'; +import { targetPaths } from '@backstage/cli-common'; export const createTypeDistProject = async () => { return new Project({ - tsConfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + tsConfigFilePath: targetPaths.resolveRoot('tsconfig.json'), skipAddingFilesFromTsConfig: true, }); }; diff --git a/packages/cli/src/modules/build/lib/urls.test.ts b/packages/cli-module-build/src/lib/urls.test.ts similarity index 100% rename from packages/cli/src/modules/build/lib/urls.test.ts rename to packages/cli-module-build/src/lib/urls.test.ts diff --git a/packages/cli/src/modules/build/lib/urls.ts b/packages/cli-module-build/src/lib/urls.ts similarity index 100% rename from packages/cli/src/modules/build/lib/urls.ts rename to packages/cli-module-build/src/lib/urls.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/.gitignore b/packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/.gitignore rename to packages/cli-module-build/src/tests/transforms/__fixtures__/.gitignore diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-default/print.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-default.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-default.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-named.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-named.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-default.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-default.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-named.cts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-named.cts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/main.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/main.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/package.json b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/package.json rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/print.ts b/packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/print.ts rename to packages/cli-module-build/src/tests/transforms/__fixtures__/pkg-module/print.ts diff --git a/packages/cli/src/tests/transforms/transforms.test.ts b/packages/cli-module-build/src/tests/transforms/transforms.test.ts similarity index 98% rename from packages/cli/src/tests/transforms/transforms.test.ts rename to packages/cli-module-build/src/tests/transforms/transforms.test.ts index 71661ded01..71ee03df76 100644 --- a/packages/cli/src/tests/transforms/transforms.test.ts +++ b/packages/cli-module-build/src/tests/transforms/transforms.test.ts @@ -16,7 +16,7 @@ import { execFileSync } from 'node:child_process'; import { resolve as resolvePath } from 'node:path'; -import { Output, buildPackage } from '../../modules/build/lib/builder'; +import { Output, buildPackage } from '../../lib/builder'; const exportValues = { all: { @@ -55,7 +55,7 @@ function loadFixture(fixture: string) { 'node', [ '--import', - '@backstage/cli/config/nodeTransform.cjs', + require.resolve('@backstage/cli-node/config/nodeTransform.cjs'), resolvePath(__dirname, `__fixtures__/${fixture}`), ], { encoding: 'utf8' }, diff --git a/packages/cli-module-build/templates/serve_index.html b/packages/cli-module-build/templates/serve_index.html new file mode 100644 index 0000000000..bc91f1bc04 --- /dev/null +++ b/packages/cli-module-build/templates/serve_index.html @@ -0,0 +1,27 @@ + + + + + + + + Backstage + + + +
    + + + diff --git a/packages/cli-module-config/.eslintrc.js b/packages/cli-module-config/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-config/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-config/README.md b/packages/cli-module-config/README.md new file mode 100644 index 0000000000..dc5b266d98 --- /dev/null +++ b/packages/cli-module-config/README.md @@ -0,0 +1,18 @@ +# @backstage/cli-module-config + +CLI module that provides configuration inspection commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :-------------- | :------------------------------------------------------------- | +| `config docs` | Browse the configuration reference documentation | +| `config:print` | Print the app configuration for the current package | +| `config:check` | Validate that the given configuration loads and matches schema | +| `config schema` | Print the JSON schema for the given configuration | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) +- [Static Configuration](https://backstage.io/docs/conf/) diff --git a/packages/cli-module-config/bin/backstage-cli-module-config b/packages/cli-module-config/bin/backstage-cli-module-config new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-config/bin/backstage-cli-module-config @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-config/catalog-info.yaml b/packages/cli-module-config/catalog-info.yaml new file mode 100644 index 0000000000..b7c5d61aba --- /dev/null +++ b/packages/cli-module-config/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-config + title: '@backstage/cli-module-config' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-config/cli-report.md b/packages/cli-module-config/cli-report.md new file mode 100644 index 0000000000..953c347951 --- /dev/null +++ b/packages/cli-module-config/cli-report.md @@ -0,0 +1,109 @@ +## CLI Report file for "@backstage/cli-module-config" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-config` + +``` +Usage: @backstage/cli-module-config [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + config [command] + config:check + config:docs + config:print + config:schema + help [command] +``` + +### `backstage-cli-module-config config` + +``` +Usage: @backstage/cli-module-config config [options] [command] [command] + +Options: + -h, --help + +Commands: + docs + help [command] + schema +``` + +### `backstage-cli-module-config config docs` + +``` +Usage: @backstage/cli-module-config config docs + +Options: + --package + -h, --help +``` + +### `backstage-cli-module-config config schema` + +``` +Usage: @backstage/cli-module-config config schema + +Options: + --format + --merge + --package + -h, --help +``` + +### `backstage-cli-module-config config:check` + +``` +Usage: @backstage/cli-module-config config:check + +Options: + --config + --deprecated + --frontend + --lax + --package + --strict + -h, --help +``` + +### `backstage-cli-module-config config:docs` + +``` +Usage: @backstage/cli-module-config config:docs + +Options: + --package + -h, --help +``` + +### `backstage-cli-module-config config:print` + +``` +Usage: @backstage/cli-module-config config:print + +Options: + --config + --format + --frontend + --lax + --package + --with-secrets + -h, --help +``` + +### `backstage-cli-module-config config:schema` + +``` +Usage: @backstage/cli-module-config config:schema + +Options: + --format + --merge + --package + -h, --help +``` diff --git a/packages/cli-module-config/package.json b/packages/cli-module-config/package.json new file mode 100644 index 0000000000..bbdf6070a3 --- /dev/null +++ b/packages/cli-module-config/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/cli-module-config", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-config" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/types": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "json-schema": "^0.4.0", + "react-dev-utils": "^12.0.0-next.60", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/json-schema": "^7.0.6" + }, + "bin": "bin/backstage-cli-module-config" +} diff --git a/packages/cli-module-config/report.api.md b/packages/cli-module-config/report.api.md new file mode 100644 index 0000000000..740aef21fb --- /dev/null +++ b/packages/cli-module-config/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-config" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli-module-config/src/commands/docs.ts similarity index 77% rename from packages/cli/src/modules/config/commands/docs.ts rename to packages/cli-module-config/src/commands/docs.ts index e7d3ac13ee..54cc1832f5 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli-module-config/src/commands/docs.ts @@ -14,20 +14,39 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; import chalk from 'chalk'; import { loadCliConfig } from '../lib/config'; +import type { CliCommandContext } from '@backstage/cli-node'; const DOCS_URL = 'https://config.backstage.io'; -export default async (opts: OptionValues) => { +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { package: pkg }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + package: { + type: String, + description: + 'Only include the schema that applies to the given package', + }, + }, + }, + undefined, + args, + ); + const { schema: appSchemas } = await loadCliConfig({ args: [], - fromPackage: opts.package, + fromPackage: pkg, mockEnv: true, }); diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli-module-config/src/commands/print.ts similarity index 58% rename from packages/cli/src/modules/config/commands/print.ts rename to packages/cli-module-config/src/commands/print.ts index 98833a7946..08f785b1d0 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli-module-config/src/commands/print.ts @@ -14,36 +14,68 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; import { loadCliConfig } from '../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { config, lax, frontend, withSecrets, format, package: pkg }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + package: { type: String, description: 'Package to print config for' }, + lax: { + type: Boolean, + description: 'Do not require environment variables to be set', + }, + frontend: { type: Boolean, description: 'Only print frontend config' }, + withSecrets: { + type: Boolean, + description: 'Include secrets in the output', + }, + format: { type: String, description: 'Output format (yaml or json)' }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + }, + }, + }, + undefined, + args, + ); -export default async (opts: OptionValues) => { const { schema, appConfigs } = await loadCliConfig({ - args: opts.config, - fromPackage: opts.package, - mockEnv: opts.lax, - fullVisibility: !opts.frontend, + args: config, + fromPackage: pkg, + mockEnv: lax, + fullVisibility: !frontend, }); - const visibility = getVisibilityOption(opts); + const visibility = getVisibilityOption(frontend, withSecrets); const data = serializeConfigData(appConfigs, schema, visibility); - if (opts.format === 'json') { + if (format === 'json') { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(data)}\n`); } }; -function getVisibilityOption(opts: OptionValues): ConfigVisibility { - if (opts.frontend && opts.withSecrets) { +function getVisibilityOption( + frontend: boolean | undefined, + withSecrets: boolean | undefined, +): ConfigVisibility { + if (frontend && withSecrets) { throw new Error('Not allowed to combine frontend and secret config'); } - if (opts.frontend) { + if (frontend) { return 'frontend'; - } else if (opts.withSecrets) { + } else if (withSecrets) { return 'secret'; } return 'backend'; diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli-module-config/src/commands/schema.ts similarity index 69% rename from packages/cli/src/modules/config/commands/schema.ts rename to packages/cli-module-config/src/commands/schema.ts index 73eb487d60..4eaa8189ba 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli-module-config/src/commands/schema.ts @@ -14,22 +14,42 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { merge, format, package: pkg }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + package: { type: String, description: 'Package to print schema for' }, + format: { type: String, description: 'Output format (yaml or json)' }, + merge: { + type: Boolean, + description: 'Merge all schemas into a single schema', + }, + }, + }, + undefined, + args, + ); -export default async (opts: OptionValues) => { const { schema } = await loadCliConfig({ args: [], - fromPackage: opts.package, + fromPackage: pkg, mockEnv: true, }); let configSchema: JsonObject | JSONSchema; - if (opts.merge) { + if (merge) { configSchema = mergeConfigSchemas( (schema.serialize().schemas as JsonObject[]).map( _ => _.value as JSONSchema, @@ -42,7 +62,7 @@ export default async (opts: OptionValues) => { configSchema = schema.serialize(); } - if (opts.format === 'json') { + if (format === 'json') { process.stdout.write(`${JSON.stringify(configSchema, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(configSchema)}\n`); diff --git a/packages/cli-module-config/src/commands/validate.ts b/packages/cli-module-config/src/commands/validate.ts new file mode 100644 index 0000000000..326c6b5e42 --- /dev/null +++ b/packages/cli-module-config/src/commands/validate.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { loadCliConfig } from '../lib/config'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { config, lax, frontend, deprecated, strict, package: pkg }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + package: { + type: String, + description: 'Package to validate config for', + }, + lax: { + type: Boolean, + description: 'Do not require environment variables to be set', + }, + frontend: { + type: Boolean, + description: 'Only validate frontend config', + }, + deprecated: { type: Boolean, description: 'Output deprecated keys' }, + strict: { type: Boolean, description: 'Enable strict validation' }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + }, + }, + }, + undefined, + args, + ); + + await loadCliConfig({ + args: config, + fromPackage: pkg, + mockEnv: lax, + fullVisibility: !frontend, + withDeprecatedKeys: deprecated, + strict, + }); +}; diff --git a/packages/cli-module-config/src/index.ts b/packages/cli-module-config/src/index.ts new file mode 100644 index 0000000000..a041f32d6e --- /dev/null +++ b/packages/cli-module-config/src/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['config:docs'], + description: 'Browse the configuration reference documentation', + execute: { loader: () => import('./commands/docs') }, + }); + reg.addCommand({ + path: ['config', 'docs'], + description: 'Browse the configuration reference documentation', + execute: { loader: () => import('./commands/docs') }, + }); + reg.addCommand({ + path: ['config:print'], + description: 'Print the app configuration for the current package', + execute: { loader: () => import('./commands/print') }, + }); + reg.addCommand({ + path: ['config:check'], + description: + 'Validate that the given configuration loads and matches schema', + execute: { loader: () => import('./commands/validate') }, + }); + reg.addCommand({ + path: ['config:schema'], + description: 'Print the JSON schema for the given configuration', + execute: { loader: () => import('./commands/schema') }, + }); + reg.addCommand({ + path: ['config', 'schema'], + description: 'Print the JSON schema for the given configuration', + execute: { loader: () => import('./commands/schema') }, + }); + }, +}); diff --git a/packages/cli/src/modules/config/lib/config.ts b/packages/cli-module-config/src/lib/config.ts similarity index 77% rename from packages/cli/src/modules/config/lib/config.ts rename to packages/cli-module-config/src/lib/config.ts index 2b313f60bd..763af2174f 100644 --- a/packages/cli/src/modules/config/lib/config.ts +++ b/packages/cli-module-config/src/lib/config.ts @@ -16,7 +16,8 @@ import { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; import { AppConfig, ConfigReader } from '@backstage/config'; -import { paths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; import { resolve as resolvePath } from 'node:path'; @@ -26,15 +27,13 @@ type Options = { targetDir?: string; fromPackage?: string; mockEnv?: boolean; - withFilteredKeys?: boolean; withDeprecatedKeys?: boolean; fullVisibility?: boolean; strict?: boolean; - watch?: (newFrontendAppConfigs: AppConfig[]) => void; }; export async function loadCliConfig(options: Options) { - const targetDir = options.targetDir ?? paths.targetDir; + const targetDir = options.targetDir ?? targetPaths.dir; // Consider all packages in the monorepo when loading in config const { packages } = await getPackages(targetDir); @@ -63,7 +62,7 @@ export async function loadCliConfig(options: Options) { const schema = await loadConfigSchema({ dependencies: localPackageNames, // Include the package.json in the project root if it exists - packagePaths: [paths.resolveTargetRoot('package.json')], + packagePaths: [targetPaths.resolveRoot('package.json')], noUndeclaredProperties: options.strict, }); @@ -72,48 +71,29 @@ export async function loadCliConfig(options: Options) { substitutionFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, - watch: Boolean(options.watch), - rootDir: paths.targetRoot, + rootDir: targetPaths.rootDir, argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]), }); const appConfigs = await new Promise((resolve, reject) => { - async function loadConfigReaderLoop() { + async function readConfig() { let loaded = false; - try { const abortController = new AbortController(); for await (const { configs } of source.readConfigData({ signal: abortController.signal, })) { - if (loaded) { - const newFrontendAppConfigs = schema.process(configs, { - visibility: options.fullVisibility - ? ['frontend', 'backend', 'secret'] - : ['frontend'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - ignoreSchemaErrors: !options.strict, - }); - options.watch?.(newFrontendAppConfigs); - } else { - resolve(configs); - loaded = true; - - if (!options.watch) { - abortController.abort(); - } - } + resolve(configs); + loaded = true; + abortController.abort(); } } catch (error) { - if (loaded) { - console.error(`Failed to reload configuration, ${error}`); - } else { + if (!loaded) { reject(error); } } } - loadConfigReaderLoop(); + readConfig(); }); const configurationLoadedMessage = appConfigs.length @@ -129,7 +109,6 @@ export async function loadCliConfig(options: Options) { visibility: options.fullVisibility ? ['frontend', 'backend', 'secret'] : ['frontend'], - withFilteredKeys: options.withFilteredKeys, withDeprecatedKeys: options.withDeprecatedKeys, ignoreSchemaErrors: !options.strict, }); diff --git a/packages/cli-module-github/.eslintrc.js b/packages/cli-module-github/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-github/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-github/README.md b/packages/cli-module-github/README.md new file mode 100644 index 0000000000..6c38376901 --- /dev/null +++ b/packages/cli-module-github/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-github + +CLI module that provides the `create-github-app` command for the Backstage CLI, used to create a new GitHub App in your organization for use with Backstage. + +## Commands + +| Command | Description | +| :------------------ | :----------------------------------------- | +| `create-github-app` | Create new GitHub App in your organization | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-github/bin/backstage-cli-module-github b/packages/cli-module-github/bin/backstage-cli-module-github new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-github/bin/backstage-cli-module-github @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-github/catalog-info.yaml b/packages/cli-module-github/catalog-info.yaml new file mode 100644 index 0000000000..b694da6f17 --- /dev/null +++ b/packages/cli-module-github/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-github + title: '@backstage/cli-module-github' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-github/cli-report.md b/packages/cli-module-github/cli-report.md new file mode 100644 index 0000000000..c606ac5988 --- /dev/null +++ b/packages/cli-module-github/cli-report.md @@ -0,0 +1,26 @@ +## CLI Report file for "@backstage/cli-module-github" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-github` + +``` +Usage: @backstage/cli-module-github [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + create-github-app + help [command] +``` + +### `backstage-cli-module-github create-github-app` + +``` +Usage: @backstage/cli-module-github create-github-app + +Options: + -h, --help +``` diff --git a/packages/cli-module-github/package.json b/packages/cli-module-github/package.json new file mode 100644 index 0000000000..1e8ca4fe6a --- /dev/null +++ b/packages/cli-module-github/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/cli-module-github", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-github" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@octokit/request": "^8.0.0", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "express": "^4.22.0", + "fs-extra": "^11.2.0", + "inquirer": "^8.2.0", + "react-dev-utils": "^12.0.0-next.60", + "yaml": "^2.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/express": "^4.17.6", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-github" +} diff --git a/packages/cli-module-github/report.api.md b/packages/cli-module-github/report.api.md new file mode 100644 index 0000000000..c389a88b5f --- /dev/null +++ b/packages/cli-module-github/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-github" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli-module-github/src/commands/create-github-app/GithubCreateAppServer.ts similarity index 100% rename from packages/cli/src/modules/create-github-app/commands/create-github-app/GithubCreateAppServer.ts rename to packages/cli-module-github/src/commands/create-github-app/GithubCreateAppServer.ts diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts b/packages/cli-module-github/src/commands/create-github-app/index.ts similarity index 88% rename from packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts rename to packages/cli-module-github/src/commands/create-github-app/index.ts index 6c0e422080..bb6896c9c1 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli-module-github/src/commands/create-github-app/index.ts @@ -18,14 +18,29 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import { stringify as stringifyYaml } from 'yaml'; import inquirer, { Question, Answers } from 'inquirer'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import { cli } from 'cleye'; + import { GithubCreateAppServer } from './GithubCreateAppServer'; import openBrowser from 'react-dev-utils/openBrowser'; +import type { CliCommandContext } from '@backstage/cli-node'; // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest -export default async (org: string) => { +export default async ({ args, info }: CliCommandContext) => { + const { _: positionals } = cli( + { + help: { ...info, usage: `${info.usage} ` }, + booleanFlagNegation: true, + parameters: [''], + }, + undefined, + args, + ); + + const org = positionals[0]; + const answers: Answers = await inquirer.prompt({ name: 'appType', type: 'checkbox', @@ -62,7 +77,7 @@ export default async (org: string) => { const fileName = `github-app-${slug}-credentials.yaml`; const content = `# Name: ${name}\n${stringifyYaml(config)}`; - await fs.writeFile(paths.resolveTargetRoot(fileName), content); + await fs.writeFile(targetPaths.resolveRoot(fileName), content); console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); console.log( chalk.yellow( diff --git a/packages/cli-module-github/src/index.ts b/packages/cli-module-github/src/index.ts new file mode 100644 index 0000000000..c3bd7cf95f --- /dev/null +++ b/packages/cli-module-github/src/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['create-github-app'], + description: 'Create new GitHub App in your organization.', + execute: { loader: () => import('./commands/create-github-app') }, + }); + }, +}); diff --git a/packages/cli-module-info/.eslintrc.js b/packages/cli-module-info/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-info/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-info/README.md b/packages/cli-module-info/README.md new file mode 100644 index 0000000000..1ba0cb3799 --- /dev/null +++ b/packages/cli-module-info/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-info + +CLI module that provides the `info` command for the Backstage CLI, displaying environment and dependency information useful for debugging and bug reports. + +## Commands + +| Command | Description | +| :------ | :-------------------------------------------------------- | +| `info` | Show helpful information for debugging and reporting bugs | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-info/bin/backstage-cli-module-info b/packages/cli-module-info/bin/backstage-cli-module-info new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-info/bin/backstage-cli-module-info @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-info/catalog-info.yaml b/packages/cli-module-info/catalog-info.yaml new file mode 100644 index 0000000000..2f6eb65952 --- /dev/null +++ b/packages/cli-module-info/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-info + title: '@backstage/cli-module-info' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-info/cli-report.md b/packages/cli-module-info/cli-report.md new file mode 100644 index 0000000000..f83583b781 --- /dev/null +++ b/packages/cli-module-info/cli-report.md @@ -0,0 +1,28 @@ +## CLI Report file for "@backstage/cli-module-info" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-info` + +``` +Usage: @backstage/cli-module-info [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + info +``` + +### `backstage-cli-module-info info` + +``` +Usage: @backstage/cli-module-info info + +Options: + --format + --include + -h, --help +``` diff --git a/packages/cli-module-info/package.json b/packages/cli-module-info/package.json new file mode 100644 index 0000000000..db19b358f4 --- /dev/null +++ b/packages/cli-module-info/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/cli-module-info", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-info" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "minimatch": "^10.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-info" +} diff --git a/packages/cli-module-info/report.api.md b/packages/cli-module-info/report.api.md new file mode 100644 index 0000000000..764b16ea47 --- /dev/null +++ b/packages/cli-module-info/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-info" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli-module-info/src/commands/info.ts similarity index 75% rename from packages/cli/src/modules/info/commands/info.ts rename to packages/cli-module-info/src/commands/info.ts index 923a02ef4c..e7307e18c2 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli-module-info/src/commands/info.ts @@ -14,19 +14,18 @@ * limitations under the License. */ -import { version as cliVersion } from '../../../../package.json'; +import { cli } from 'cleye'; +import { version as infoModuleVersion } from '../../package.json'; import os from 'node:os'; -import { runOutput } from '@backstage/cli-common'; -import { paths } from '../../../lib/paths'; -import { Lockfile } from '../../../lib/versioning'; -import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; +import { + BackstagePackageJson, + Lockfile, + PackageGraph, +} from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; - -interface InfoOptions { - include: string[]; - format: 'text' | 'json'; -} +import type { CliCommandContext } from '@backstage/cli-node'; /** * Attempts to read package.json from node_modules for a given package name. @@ -53,12 +52,38 @@ function hasBackstageField(packageName: string, targetPath: string): boolean { return pkg?.backstage !== undefined; } -export default async (options: InfoOptions) => { +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { include, format }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + include: { + type: [String], + description: + 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', + }, + format: { + type: String, + description: 'Output format (text or json)', + default: 'text', + }, + }, + }, + undefined, + args, + ); + + const options = { include, format: format as 'text' | 'json' }; + await new Promise(async () => { const yarnVersion = await runOutput(['yarn', '--version']); - const isLocal = fs.existsSync(paths.resolveOwn('./src')); + /* eslint-disable-next-line no-restricted-syntax */ + const isLocal = fs.existsSync(findOwnPaths(__dirname).resolve('./src')); - const backstageFile = paths.resolveTargetRoot('backstage.json'); + const backstageFile = targetPaths.resolveRoot('backstage.json'); let backstageVersion = 'N/A'; if (fs.existsSync(backstageFile)) { try { @@ -74,18 +99,33 @@ export default async (options: InfoOptions) => { } } - // Build system info + // Try to resolve the CLI package version — it may not be installed + // when this module is executed standalone. + let cliVersion: string | undefined; + try { + cliVersion = ( + require(require.resolve('@backstage/cli/package.json', { + paths: [process.cwd()], + })) as { version: string } + ).version; + } catch { + /* not available */ + } + const systemInfo = { os: `${os.type} ${os.release} - ${os.platform}/${os.arch}`, node: process.version, yarn: yarnVersion, - cli: { version: cliVersion, local: isLocal }, + ...(cliVersion + ? { cli: { version: cliVersion, local: isLocal } } + : undefined), + infoModuleVersion, backstage: backstageVersion, }; - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfilePath = targetPaths.resolveRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); - const targetPath = paths.targetRoot; + const targetPath = targetPaths.rootDir; // Get workspace package names and their versions const workspacePackages = new Map(); @@ -183,8 +223,11 @@ export default async (options: InfoOptions) => { console.log(`OS: ${systemInfo.os}`); console.log(`node: ${systemInfo.node}`); console.log(`yarn: ${systemInfo.yarn}`); - console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); - console.log(`backstage: ${backstageVersion}`); + if (cliVersion) { + console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); + } + console.log(`info module: ${infoModuleVersion}`); + console.log(`backstage: ${backstageVersion}`); console.log(); // Print installed dependencies diff --git a/packages/ui/src/components/HeaderPage/definition.ts b/packages/cli-module-info/src/index.ts similarity index 59% rename from packages/ui/src/components/HeaderPage/definition.ts rename to packages/cli-module-info/src/index.ts index be93aaae6a..c7ace24bbd 100644 --- a/packages/ui/src/components/HeaderPage/definition.ts +++ b/packages/cli-module-info/src/index.ts @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; -import type { ComponentDefinition } from '../../types'; - -/** - * Component definition for HeaderPage - * @public - */ -export const HeaderPageDefinition = { - classNames: { - root: 'bui-HeaderPage', - content: 'bui-HeaderPageContent', - breadcrumbs: 'bui-HeaderPageBreadcrumbs', - tabsWrapper: 'bui-HeaderPageTabsWrapper', - controls: 'bui-HeaderPageControls', +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['info'], + description: 'Show helpful information for debugging and reporting bugs', + execute: { loader: () => import('./commands/info') }, + }); }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/cli-module-lint/.eslintrc.js b/packages/cli-module-lint/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-lint/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-lint/README.md b/packages/cli-module-lint/README.md new file mode 100644 index 0000000000..23d1f06e7e --- /dev/null +++ b/packages/cli-module-lint/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-lint + +CLI module that provides linting commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :------------- | :---------------- | +| `package lint` | Lint a package | +| `repo lint` | Lint a repository | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-lint/bin/backstage-cli-module-lint b/packages/cli-module-lint/bin/backstage-cli-module-lint new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-lint/bin/backstage-cli-module-lint @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-lint/catalog-info.yaml b/packages/cli-module-lint/catalog-info.yaml new file mode 100644 index 0000000000..d40b7e23e3 --- /dev/null +++ b/packages/cli-module-lint/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-lint + title: '@backstage/cli-module-lint' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-lint/cli-report.md b/packages/cli-module-lint/cli-report.md new file mode 100644 index 0000000000..2a9ff6d61f --- /dev/null +++ b/packages/cli-module-lint/cli-report.md @@ -0,0 +1,73 @@ +## CLI Report file for "@backstage/cli-module-lint" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-lint` + +``` +Usage: @backstage/cli-module-lint [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-lint package` + +``` +Usage: @backstage/cli-module-lint package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `backstage-cli-module-lint package lint` + +``` +Usage: @backstage/cli-module-lint package lint [directories...] + +Options: + --fix + --format + --max-warnings + --output-file + -h, --help +``` + +### `backstage-cli-module-lint repo` + +``` +Usage: @backstage/cli-module-lint repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + lint +``` + +### `backstage-cli-module-lint repo lint` + +``` +Usage: @backstage/cli-module-lint repo lint + +Options: + --fix + --format + --max-warnings + --output-file + --since + --success-cache + --success-cache-dir + -h, --help +``` diff --git a/packages/cli-module-lint/package.json b/packages/cli-module-lint/package.json new file mode 100644 index 0000000000..452e10eeed --- /dev/null +++ b/packages/cli-module-lint/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/cli-module-lint", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-lint" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "eslint": "^8.6.0", + "eslint-formatter-friendly": "^7.0.0", + "fs-extra": "^11.2.0", + "globby": "^11.1.0", + "shell-quote": "^1.8.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/shell-quote": "^1.7.5" + }, + "bin": "bin/backstage-cli-module-lint" +} diff --git a/packages/cli-module-lint/report.api.md b/packages/cli-module-lint/report.api.md new file mode 100644 index 0000000000..4a861a27a0 --- /dev/null +++ b/packages/cli-module-lint/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-lint" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-lint/src/commands/package/lint.ts b/packages/cli-module-lint/src/commands/package/lint.ts new file mode 100644 index 0000000000..7fabf0c0ff --- /dev/null +++ b/packages/cli-module-lint/src/commands/package/lint.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { targetPaths } from '@backstage/cli-common'; +import { ESLint } from 'eslint'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { fix, format, outputFile, maxWarnings }, + _: directories, + } = cli( + { + help: { ...info, usage: `${info.usage} [directories...]` }, + booleanFlagNegation: true, + parameters: ['[directories...]'], + flags: { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }, + }, + undefined, + args, + ); + + const eslint = new ESLint({ + cwd: targetPaths.dir, + fix, + extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + + const results = await eslint.lintFiles( + directories.length ? directories : ['.'], + ); + + const maxWarningsNum = maxWarnings ? +maxWarnings : -1; + const ignoreWarnings = maxWarningsNum === -1; + + const failed = + results.some(r => r.errorCount > 0) || + (!ignoreWarnings && + results.reduce((current, next) => current + next.warningCount, 0) > + maxWarningsNum); + + if (fix) { + await ESLint.outputFixes(results); + } + + const formatter = await eslint.loadFormatter(format); + + // This formatter uses the cwd to format file paths, so let's have that happen from the root instead + if (format === 'eslint-formatter-friendly') { + process.chdir(targetPaths.rootDir); + } + + const resultText = await formatter.format(results); + + if (resultText) { + if (outputFile) { + await fs.writeFile(targetPaths.resolve(outputFile), resultText); + } else { + console.log(resultText); + } + } + + if (failed) { + process.exit(1); + } +}; diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli-module-lint/src/commands/repo/lint.ts similarity index 67% rename from packages/cli/src/modules/lint/commands/repo/lint.ts rename to packages/cli-module-lint/src/commands/repo/lint.ts index 458ccf1a63..01dfad6339 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli-module-lint/src/commands/repo/lint.ts @@ -16,18 +16,20 @@ import chalk from 'chalk'; import fs from 'fs-extra'; -import { Command, OptionValues } from 'commander'; +import { cli } from 'cleye'; import { createHash } from 'node:crypto'; import { relative as relativePath } from 'node:path'; import { PackageGraph, BackstagePackageJson, Lockfile, + runWorkerQueueThreads, + SuccessCache, } from '@backstage/cli-node'; -import { paths } from '../../../../lib/paths'; -import { runWorkerQueueThreads } from '../../../../lib/parallel'; -import { createScriptOptionsParser } from '../../../../lib/optionsParser'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; +import { targetPaths } from '@backstage/cli-common'; + +import { createScriptOptionsParser } from '../../lib/optionsParser'; +import type { CliCommandContext } from '@backstage/cli-node'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -37,21 +39,91 @@ function depCount(pkg: BackstagePackageJson) { return deps + devDeps; } -export async function command(opts: OptionValues, cmd: Command): Promise { +export default async ({ args, info }: CliCommandContext) => { + for (const flag of [ + 'outputFile', + 'successCache', + 'successCacheDir', + 'maxWarnings', + ]) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } + } + + const { + flags: { + fix, + format, + outputFile, + successCache: useSuccessCache, + successCacheDir, + since, + maxWarnings, + }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + successCache: { + type: Boolean, + description: + 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', + }, + successCacheDir: { + type: String, + description: + 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', + }, + since: { + type: String, + description: + 'Only lint packages that changed since the specified ref', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }, + }, + undefined, + args, + ); + let packages = await PackageGraph.listTargetPackages(); - const cache = new SuccessCache('lint', opts.successCacheDir); - const cacheContext = opts.successCache + const cache = SuccessCache.create({ + name: 'lint', + basePath: successCacheDir, + }); + const cacheContext = useSuccessCache ? { entries: await cache.read(), - lockfile: await Lockfile.load(paths.resolveTargetRoot('yarn.lock')), + lockfile: await Lockfile.load(targetPaths.resolveRoot('yarn.lock')), } : undefined; - if (opts.since) { + if (since) { const graph = PackageGraph.fromPackages(packages); packages = await graph.listChangedPackages({ - ref: opts.since, + ref: since, analyzeLockfile: true, }); } @@ -61,8 +133,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { packages.sort((a, b) => depCount(b.packageJson) - depCount(a.packageJson)); // This formatter uses the cwd to format file paths, so let's have that happen from the root instead - if (opts.format === 'eslint-formatter-friendly') { - process.chdir(paths.targetRoot); + if (format === 'eslint-formatter-friendly') { + process.chdir(targetPaths.rootDir); } // Make sure lint output is colored unless the user explicitly disabled it @@ -70,14 +142,19 @@ export async function command(opts: OptionValues, cmd: Command): Promise { process.env.FORCE_COLOR = '1'; } - const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']); + const parseLintScript = createScriptOptionsParser(['package', 'lint'], { + fix: { type: 'boolean' }, + format: { type: 'string' }, + 'output-file': { type: 'string' }, + 'max-warnings': { type: 'string' }, + }); const items = await Promise.all( packages.map(async pkg => { const lintOptions = parseLintScript(pkg.packageJson.scripts?.lint); const base = { fullDir: pkg.dir, - relativeDir: relativePath(paths.targetRoot, pkg.dir), + relativeDir: relativePath(targetPaths.rootDir, pkg.dir), lintOptions, parentHash: undefined, }; @@ -105,23 +182,23 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }), ); - const resultsList = await runWorkerQueueThreads({ + const { results: resultsList } = await runWorkerQueueThreads({ items: items.filter(item => item.lintOptions), // Filter out packages without lint script - workerData: { - fix: Boolean(opts.fix), - format: opts.format as string | undefined, + context: { + fix: Boolean(fix), + format: format as string | undefined, shouldCache: Boolean(cacheContext), - maxWarnings: opts.maxWarnings ?? -1, + maxWarnings: maxWarnings ?? '-1', successCache: cacheContext?.entries, - rootDir: paths.targetRoot, + rootDir: targetPaths.rootDir, }, workerFactory: async ({ - fix, - format, + fix: workerFix, + format: workerFormat, shouldCache, successCache, rootDir, - maxWarnings, + maxWarnings: workerMaxWarnings, }) => { const { ESLint } = require('eslint') as typeof import('eslint'); const crypto = require('node:crypto') as typeof import('crypto'); @@ -147,7 +224,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const start = Date.now(); const eslint = new ESLint({ cwd: fullDir, - fix, + fix: workerFix, extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], }); @@ -188,7 +265,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } } - const formatter = await eslint.loadFormatter(format); + const formatter = await eslint.loadFormatter(workerFormat); const results = await eslint.lintFiles(['.']); @@ -196,18 +273,18 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const time = ((Date.now() - start) / 1000).toFixed(2); console.log(`Checked ${count} files in ${relativeDir} ${time}s`); - if (fix) { + if (workerFix) { await ESLint.outputFixes(results); } - const ignoreWarnings = +maxWarnings === -1; + const ignoreWarnings = +workerMaxWarnings === -1; const resultText = formatter.format(results) as string; const failed = results.some(r => r.errorCount > 0) || (!ignoreWarnings && results.reduce((current, next) => current + next.warningCount, 0) > - maxWarnings); + +workerMaxWarnings); return { relativeDir, @@ -238,8 +315,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { // When doing repo lint, only list the results if the lint failed to avoid a log // dump of all warnings that might be irrelevant if (resultText) { - if (opts.outputFile) { - if (opts.format === 'json') { + if (outputFile) { + if (format === 'json') { jsonResults.push(resultText); } else { errorOutput += `${resultText}\n`; @@ -254,7 +331,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } } - if (opts.format === 'json') { + if (format === 'json') { let mergedJsonResults: any[] = []; for (const jsonResult of jsonResults) { mergedJsonResults = mergedJsonResults.concat(JSON.parse(jsonResult)); @@ -262,8 +339,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { errorOutput = JSON.stringify(mergedJsonResults, null, 2); } - if (opts.outputFile && errorOutput) { - await fs.writeFile(paths.resolveTargetRoot(opts.outputFile), errorOutput); + if (outputFile && errorOutput) { + await fs.writeFile(targetPaths.resolveRoot(outputFile), errorOutput); } if (cacheContext) { @@ -273,4 +350,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise { if (failed) { process.exit(1); } -} +}; diff --git a/packages/cli-module-lint/src/index.ts b/packages/cli-module-lint/src/index.ts new file mode 100644 index 0000000000..d202841e79 --- /dev/null +++ b/packages/cli-module-lint/src/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['package', 'lint'], + description: 'Lint a package', + execute: { loader: () => import('./commands/package/lint') }, + }); + + reg.addCommand({ + path: ['repo', 'lint'], + description: 'Lint a repository', + execute: { loader: () => import('./commands/repo/lint') }, + }); + }, +}); diff --git a/packages/cli-module-lint/src/lib/optionsParser.ts b/packages/cli-module-lint/src/lib/optionsParser.ts new file mode 100644 index 0000000000..62c3b7ce71 --- /dev/null +++ b/packages/cli-module-lint/src/lib/optionsParser.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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 { parseArgs, type ParseArgsConfig } from 'node:util'; +import { parse as parseShellArgs } from 'shell-quote'; + +export function createScriptOptionsParser( + commandPath: string[], + options: ParseArgsConfig['options'], +) { + const expectedScript = `backstage-cli ${commandPath.join(' ')}`; + + return (scriptStr?: string) => { + if (!scriptStr || !scriptStr.startsWith(expectedScript)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedScript.length).trim(); + const args = argsStr + ? parseShellArgs(argsStr).filter( + (e): e is string => typeof e === 'string', + ) + : []; + + const { values } = parseArgs({ args, strict: false, options }); + return values; + }; +} diff --git a/packages/cli-module-maintenance/.eslintrc.js b/packages/cli-module-maintenance/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-maintenance/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-maintenance/README.md b/packages/cli-module-maintenance/README.md new file mode 100644 index 0000000000..f13e78a32e --- /dev/null +++ b/packages/cli-module-maintenance/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-maintenance + +CLI module that provides repository maintenance commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------------- | :---------------------------------------- | +| `repo fix` | Automatically fix packages in the project | +| `repo list-deprecations` | List deprecations | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-maintenance/bin/backstage-cli-module-maintenance @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-maintenance/catalog-info.yaml b/packages/cli-module-maintenance/catalog-info.yaml new file mode 100644 index 0000000000..df05034045 --- /dev/null +++ b/packages/cli-module-maintenance/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-maintenance + title: '@backstage/cli-module-maintenance' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-maintenance/cli-report.md b/packages/cli-module-maintenance/cli-report.md new file mode 100644 index 0000000000..874d9d781d --- /dev/null +++ b/packages/cli-module-maintenance/cli-report.md @@ -0,0 +1,52 @@ +## CLI Report file for "@backstage/cli-module-maintenance" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-maintenance` + +``` +Usage: @backstage/cli-module-maintenance [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + repo [command] +``` + +### `backstage-cli-module-maintenance repo` + +``` +Usage: @backstage/cli-module-maintenance repo [options] [command] [command] + +Options: + -h, --help + +Commands: + fix + help [command] + list-deprecations +``` + +### `backstage-cli-module-maintenance repo fix` + +``` +Usage: @backstage/cli-module-maintenance repo fix + +Options: + --check + --publish + -h, --help +``` + +### `backstage-cli-module-maintenance repo list-deprecations` + +``` +Usage: @backstage/cli-module-maintenance repo list-deprecations + +Options: + --json + -h, --help +``` diff --git a/packages/cli-module-maintenance/package.json b/packages/cli-module-maintenance/package.json new file mode 100644 index 0000000000..24f7a9b70a --- /dev/null +++ b/packages/cli-module-maintenance/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/cli-module-maintenance", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-maintenance" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "eslint": "^8.6.0", + "fs-extra": "^11.2.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-maintenance" +} diff --git a/packages/cli-module-maintenance/report.api.md b/packages/cli-module-maintenance/report.api.md new file mode 100644 index 0000000000..9eb59483d7 --- /dev/null +++ b/packages/cli-module-maintenance/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-maintenance" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli-module-maintenance/src/commands/repo/fix.ts similarity index 92% rename from packages/cli/src/modules/maintenance/commands/repo/fix.ts rename to packages/cli-module-maintenance/src/commands/repo/fix.ts index a5c6b519ed..4d6445fe86 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli-module-maintenance/src/commands/repo/fix.ts @@ -21,7 +21,7 @@ import { PackageRole, PackageRoles, } from '@backstage/cli-node'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath, @@ -29,8 +29,7 @@ import { relative as relativePath, extname, } from 'node:path'; -import { paths } from '../../../../lib/paths'; -import { publishPreflightCheck } from '../../lib/publishing'; +import { targetPaths } from '@backstage/cli-common'; const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.json']; @@ -49,7 +48,7 @@ export async function readFixablePackages(): Promise { export function printPackageFixHint(packages: FixablePackage[]) { const changed = packages.filter(pkg => pkg.changed); if (changed.length > 0) { - const rootPkg = require(paths.resolveTargetRoot('package.json')); + const rootPkg = require(targetPaths.resolveRoot('package.json')); const fixCmd = rootPkg.scripts?.fix === 'backstage-cli repo fix' ? 'fix' @@ -216,7 +215,7 @@ export function fixSideEffects(pkg: FixablePackage) { } export function createRepositoryFieldFixer() { - const rootPkg = require(paths.resolveTargetRoot('package.json')); + const rootPkg = require(targetPaths.resolveRoot('package.json')); const rootRepoField = rootPkg.repository; if (!rootRepoField) { return () => {}; @@ -229,7 +228,7 @@ export function createRepositoryFieldFixer() { return (pkg: FixablePackage) => { const expectedPath = posix.join( rootDir, - relativePath(paths.targetRoot, pkg.dir), + relativePath(targetPaths.rootDir, pkg.dir), ); const repoField = pkg.packageJson.repository; @@ -295,7 +294,12 @@ export function fixPluginId(pkg: FixablePackage) { return; } - if (role === 'backend' || role === 'frontend' || role === 'cli') { + if ( + role === 'backend' || + role === 'frontend' || + role === 'cli' || + role === 'cli-module' + ) { return; } @@ -318,7 +322,7 @@ export function fixPluginId(pkg: FixablePackage) { role === 'backend-plugin-module') ) { const path = relativePath( - paths.targetRoot, + targetPaths.rootDir, resolvePath(pkg.dir, 'package.json'), ); const msg = `Failed to guess plugin ID for "${pkg.packageJson.name}", please set the 'backstage.pluginId' field manually in "${path}"`; @@ -377,7 +381,12 @@ export function fixPluginPackages( return; } - if (role === 'backend' || role === 'frontend' || role === 'cli') { + if ( + role === 'backend' || + role === 'frontend' || + role === 'cli' || + role === 'cli-module' + ) { return; } @@ -414,7 +423,7 @@ export function fixPluginPackages( return; } const path = relativePath( - paths.targetRoot, + targetPaths.rootDir, resolvePath(pkg.dir, 'package.json'), ); const suggestedRole = @@ -463,7 +472,7 @@ export function fixPeerModules(pkg: FixablePackage) { } const packagePath = relativePath( - paths.targetRoot, + targetPaths.rootDir, resolvePath(pkg.dir, 'package.json'), ); @@ -493,21 +502,44 @@ export function fixPeerModules(pkg: FixablePackage) { type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; -export async function command(opts: OptionValues): Promise { +export default async ({ + args, + info, +}: import('@backstage/cli-node').CliCommandContext) => { + const { + flags: { publish, check }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + publish: { + type: Boolean, + description: + 'Enable additional fixes that only apply when publishing packages', + }, + check: { + type: Boolean, + description: + 'Fail if any packages would have been changed by the command', + }, + }, + }, + undefined, + args, + ); + const packages = await readFixablePackages(); const fixRepositoryField = createRepositoryFieldFixer(); const fixers: PackageFixer[] = [fixPackageExports, fixSideEffects]; - // Fixers that only apply to repos that publish packages - if (opts.publish) { + if (publish) { fixers.push( fixRepositoryField, fixPluginId, fixPluginPackages, fixPeerModules, - // Run the publish preflight check too, to make sure we don't uncover errors during publishing - publishPreflightCheck, ); } @@ -517,11 +549,11 @@ export async function command(opts: OptionValues): Promise { } } - if (opts.check) { + if (check) { if (printPackageFixHint(packages)) { process.exit(1); } } else { await writeFixedPackages(packages); } -} +}; diff --git a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts b/packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts similarity index 78% rename from packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts rename to packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts index b89058e9fb..e711cb32be 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts +++ b/packages/cli-module-maintenance/src/commands/repo/list-deprecations.ts @@ -16,23 +16,38 @@ import chalk from 'chalk'; import { ESLint } from 'eslint'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { json }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + json: { type: Boolean, description: 'Output as JSON' }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues) { const packages = await PackageGraph.listTargetPackages(); const eslint = new ESLint({ - cwd: paths.targetDir, + cwd: targetPaths.dir, overrideConfig: { plugins: ['deprecation'], rules: { 'deprecation/deprecation': 'error', }, parserOptions: { - project: [paths.resolveTargetRoot('tsconfig.json')], + project: [targetPaths.resolveRoot('tsconfig.json')], }, }, extensions: ['jsx', 'ts', 'tsx', 'mjs', 'cjs'], @@ -52,7 +67,7 @@ export async function command(opts: OptionValues) { continue; } - const path = relativePath(paths.targetRoot, result.filePath); + const path = relativePath(targetPaths.rootDir, result.filePath); deprecations.push({ path, message: message.message, @@ -74,7 +89,7 @@ export async function command(opts: OptionValues) { stderr.cursorTo(0); } - if (opts.json) { + if (json) { console.log(JSON.stringify(deprecations, null, 2)); } else { for (const d of deprecations) { @@ -87,4 +102,4 @@ export async function command(opts: OptionValues) { if (deprecations.length > 0) { process.exit(1); } -} +}; diff --git a/packages/cli-module-maintenance/src/index.ts b/packages/cli-module-maintenance/src/index.ts new file mode 100644 index 0000000000..77304eef47 --- /dev/null +++ b/packages/cli-module-maintenance/src/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['repo', 'fix'], + description: 'Automatically fix packages in the project', + execute: { loader: () => import('./commands/repo/fix') }, + }); + + reg.addCommand({ + path: ['repo', 'list-deprecations'], + description: 'List deprecations', + execute: { loader: () => import('./commands/repo/list-deprecations') }, + }); + }, +}); diff --git a/packages/cli-module-migrate/.eslintrc.js b/packages/cli-module-migrate/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-migrate/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-migrate/README.md b/packages/cli-module-migrate/README.md new file mode 100644 index 0000000000..842f0e645c --- /dev/null +++ b/packages/cli-module-migrate/README.md @@ -0,0 +1,20 @@ +# @backstage/cli-module-migrate + +CLI module that provides migration and version management commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :----------------------------- | :---------------------------------------------------------------- | +| `versions:bump` | Bump Backstage packages to the latest versions | +| `versions:migrate` | Migrate plugins moved to the @backstage-community namespace | +| `migrate package-roles` | Add package role field to packages that don't have it | +| `migrate package-scripts` | Set package scripts according to each package role | +| `migrate package-exports` | Synchronize package subpath export definitions | +| `migrate package-lint-configs` | Migrates all packages to use @backstage/cli/config/eslint-factory | +| `migrate react-router-deps` | Migrates the react-router dependencies to be peer dependencies | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-migrate/bin/backstage-cli-module-migrate b/packages/cli-module-migrate/bin/backstage-cli-module-migrate new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-migrate/bin/backstage-cli-module-migrate @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-migrate/catalog-info.yaml b/packages/cli-module-migrate/catalog-info.yaml new file mode 100644 index 0000000000..5c62edf9ba --- /dev/null +++ b/packages/cli-module-migrate/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-migrate + title: '@backstage/cli-module-migrate' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-migrate/cli-report.md b/packages/cli-module-migrate/cli-report.md new file mode 100644 index 0000000000..f0c29e1edd --- /dev/null +++ b/packages/cli-module-migrate/cli-report.md @@ -0,0 +1,105 @@ +## CLI Report file for "@backstage/cli-module-migrate" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-migrate` + +``` +Usage: @backstage/cli-module-migrate [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + migrate [command] + versions:bump + versions:migrate +``` + +### `backstage-cli-module-migrate migrate` + +``` +Usage: @backstage/cli-module-migrate migrate [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + package-exports + package-lint-configs + package-roles + package-scripts + react-router-deps +``` + +### `backstage-cli-module-migrate migrate package-exports` + +``` +Usage: @backstage/cli-module-migrate migrate package-exports + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-lint-configs` + +``` +Usage: @backstage/cli-module-migrate migrate package-lint-configs + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-roles` + +``` +Usage: @backstage/cli-module-migrate migrate package-roles + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate package-scripts` + +``` +Usage: @backstage/cli-module-migrate migrate package-scripts + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate migrate react-router-deps` + +``` +Usage: @backstage/cli-module-migrate migrate react-router-deps + +Options: + -h, --help +``` + +### `backstage-cli-module-migrate versions:bump` + +``` +Usage: @backstage/cli-module-migrate versions:bump + +Options: + --pattern + --release + --skip-install + --skip-migrate + -h, --help +``` + +### `backstage-cli-module-migrate versions:migrate` + +``` +Usage: @backstage/cli-module-migrate versions:migrate + +Options: + --pattern + --skip-code-changes + -h, --help +``` diff --git a/packages/cli-module-migrate/package.json b/packages/cli-module-migrate/package.json new file mode 100644 index 0000000000..dfc155fd11 --- /dev/null +++ b/packages/cli-module-migrate/package.json @@ -0,0 +1,57 @@ +{ + "name": "@backstage/cli-module-migrate", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-migrate" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/release-manifests": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "minimatch": "^10.2.1", + "ora": "^5.3.0", + "replace-in-file": "^7.1.0", + "semver": "^7.5.3" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/semver": "^7", + "msw": "^1.0.0" + }, + "bin": "bin/backstage-cli-module-migrate" +} diff --git a/packages/cli-module-migrate/report.api.md b/packages/cli-module-migrate/report.api.md new file mode 100644 index 0000000000..09ed6c902a --- /dev/null +++ b/packages/cli-module-migrate/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-migrate" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-migrate/src/commands/packageExports.ts b/packages/cli-module-migrate/src/commands/packageExports.ts new file mode 100644 index 0000000000..8a4ad45f42 --- /dev/null +++ b/packages/cli-module-migrate/src/commands/packageExports.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); + throw new Error( + 'The `migrate package-exports` command has been removed, use `repo fix` instead.', + ); +}; diff --git a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts similarity index 92% rename from packages/cli/src/modules/migrate/commands/packageLintConfigs.ts rename to packages/cli-module-migrate/src/commands/packageLintConfigs.ts index d6e6183b83..660ed35bb6 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli-module-migrate/src/commands/packageLintConfigs.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { runOutput } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; -export async function command() { +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); const oldConfigs = [ @@ -86,4 +89,4 @@ export async function command() { if (hasPrettier) { await runOutput(['prettier', '--write', ...configPaths]); } -} +}; diff --git a/packages/cli/src/modules/migrate/commands/packageRole.ts b/packages/cli-module-migrate/src/commands/packageRole.ts similarity index 86% rename from packages/cli/src/modules/migrate/commands/packageRole.ts rename to packages/cli-module-migrate/src/commands/packageRole.ts index 58c1487149..8758d48dd5 100644 --- a/packages/cli/src/modules/migrate/commands/packageRole.ts +++ b/packages/cli-module-migrate/src/commands/packageRole.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageRoles } from '@backstage/cli-node'; -import { paths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; -export default async () => { - const { packages } = await getPackages(paths.targetDir); +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); + const { packages } = await getPackages(targetPaths.dir); await Promise.all( packages.map(async ({ dir, packageJson: pkg }) => { diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli-module-migrate/src/commands/packageScripts.ts similarity index 87% rename from packages/cli/src/modules/migrate/commands/packageScripts.ts rename to packages/cli-module-migrate/src/commands/packageScripts.ts index 6c4e41c3ec..94a05f67f9 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli-module-migrate/src/commands/packageScripts.ts @@ -14,15 +14,18 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node'; +import type { CliCommandContext } from '@backstage/cli-node'; const configArgPattern = /--config[=\s][^\s$]+/; -const noStartRoles: PackageRole[] = ['cli', 'common-library']; +const noStartRoles: PackageRole[] = ['cli', 'cli-module', 'common-library']; -export async function command() { +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); await Promise.all( @@ -56,14 +59,14 @@ export async function command() { // For test scripts we keep all existing flags except for --passWithNoTests, since that's now default const testCmd = ['test']; if (scripts.test?.startsWith('backstage-cli test')) { - const args = scripts.test + const testArgs = scripts.test .slice('backstage-cli test'.length) .split(' ') .filter(Boolean); - if (args.includes('--passWithNoTests')) { - args.splice(args.indexOf('--passWithNoTests'), 1); + if (testArgs.includes('--passWithNoTests')) { + testArgs.splice(testArgs.indexOf('--passWithNoTests'), 1); } - testCmd.push(...args); + testCmd.push(...testArgs); } const expectedScripts = { @@ -104,4 +107,4 @@ export async function command() { } }), ); -} +}; diff --git a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts similarity index 89% rename from packages/cli/src/modules/migrate/commands/reactRouterDeps.ts rename to packages/cli-module-migrate/src/commands/reactRouterDeps.ts index 3f187dd534..802d958e61 100644 --- a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts +++ b/packages/cli-module-migrate/src/commands/reactRouterDeps.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles } from '@backstage/cli-node'; +import type { CliCommandContext } from '@backstage/cli-node'; const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom']; const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0'; -export async function command() { +export default async ({ args, info }: CliCommandContext) => { + cli({ help: info, booleanFlagNegation: true }, undefined, args); const packages = await PackageGraph.listTargetPackages(); await Promise.all( @@ -56,4 +59,4 @@ export async function command() { } }), ); -} +}; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli-module-migrate/src/commands/versions/bump.test.ts similarity index 93% rename from packages/cli/src/modules/migrate/commands/versions/bump.test.ts rename to packages/cli-module-migrate/src/commands/versions/bump.test.ts index 202704e030..c7694f5e09 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli-module-migrate/src/commands/versions/bump.test.ts @@ -14,18 +14,15 @@ * limitations under the License. */ import fs from 'fs-extra'; -import { Command } from 'commander'; import * as runObj from '@backstage/cli-common'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; -import { YarnInfoInspectData } from '../../../../lib/versioning/packages'; +import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { NotFoundError } from '@backstage/errors'; -import { - createMockDirectory, - MockDirectory, -} from '@backstage/backend-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; // Avoid mutating the global agents used in other tests jest.mock('global-agent', () => ({ @@ -59,19 +56,10 @@ jest.mock('ora', () => ({ }, })); -let mockDir: MockDirectory; jest.mock('@backstage/cli-common', () => { const actual = jest.requireActual('@backstage/cli-common'); return { ...actual, - findPaths: () => ({ - resolveTargetRoot(filename: string) { - return mockDir.resolve(filename); - }, - get targetDir() { - return mockDir.path; - }, - }), run: jest.fn().mockReturnValue({ exitCode: null, waitForExit: jest.fn().mockResolvedValue(undefined), @@ -80,8 +68,8 @@ jest.mock('@backstage/cli-common', () => { }); const mockFetchPackageInfo = jest.fn(); -jest.mock('../../../../lib/versioning/packages', () => { - const actual = jest.requireActual('../../../../lib/versioning/packages'); +jest.mock('../../lib/versioning/packages', () => { + const actual = jest.requireActual('../../lib/versioning/packages'); return { ...actual, fetchPackageInfo: (name: string) => mockFetchPackageInfo(name), @@ -137,10 +125,13 @@ const expectLogsToMatch = ( expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort()); }; +const info = { usage: 'backstage-cli versions:bump', name: 'versions:bump' }; + describe('bump', () => { - mockDir = createMockDirectory(); + const mockDir = createMockDirectory(); beforeEach(() => { + overrideTargetPaths(mockDir.path); mockFetchPackageInfo.mockImplementation(async name => ({ name: name, 'dist-tags': { @@ -160,9 +151,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -202,7 +191,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -255,9 +244,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -297,11 +284,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ - pattern: null, - release: 'main', - skipInstall: true, - } as unknown as Command); + await bump({ args: ['--release', 'main', '--skip-install'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -353,9 +336,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -405,7 +386,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -459,9 +440,7 @@ describe('bump', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -511,7 +490,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -572,9 +551,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -609,7 +586,7 @@ describe('bump', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { await expect( - bump({ pattern: null, release: '999.0.1' } as unknown as Command), + bump({ args: ['--release', '999.0.1'], info }), ).rejects.toThrow('No release found for 999.0.1 version'); }); expect(logs.filter(Boolean)).toEqual([ @@ -644,9 +621,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -716,7 +691,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'next' } as unknown as Command); + await bump({ args: ['--release', 'next'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -750,9 +725,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': customLockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -798,9 +771,14 @@ describe('bump', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { await bump({ - pattern: '@{backstage,backstage-extra}/*', - release: 'main', - } as any); + args: [ + '--pattern', + '@{backstage,backstage-extra}/*', + '--release', + 'main', + ], + info, + }); }); expectLogsToMatch(logs, [ 'Using custom pattern glob @{backstage,backstage-extra}/*', @@ -865,9 +843,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -908,7 +884,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -944,7 +920,11 @@ describe('bump', () => { }); describe('bumpBackstageJsonVersion', () => { - mockDir = createMockDirectory(); + const mockDir = createMockDirectory(); + + beforeEach(() => { + overrideTargetPaths(mockDir.path); + }); afterEach(() => { jest.resetAllMocks(); @@ -1078,9 +1058,15 @@ describe('createVersionFinder', () => { }); describe('environment variables', () => { + const mockDir = createMockDirectory(); + const worker = setupServer(); registerMswTestHooks(worker); + beforeEach(() => { + overrideTargetPaths(mockDir.path); + }); + beforeEach(() => { delete process.env.BACKSTAGE_MANIFEST_FILE; process.env.BACKSTAGE_VERSIONS_BASE_URL = 'https://custom.example.com'; @@ -1099,9 +1085,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1139,7 +1123,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1192,9 +1176,7 @@ describe('environment variables', () => { 'custom-manifest.json': JSON.stringify(customManifest), 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1215,7 +1197,7 @@ describe('environment variables', () => { } as any); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1262,9 +1244,7 @@ describe('environment variables', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1302,7 +1282,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1337,9 +1317,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1353,18 +1331,14 @@ describe('environment variables', () => { }, }); - await expect( - bump({ pattern: null, release: 'main' } as unknown as Command), - ).rejects.toThrow(); + await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow(); }); it('should handle network errors when using custom base URL', async () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1385,8 +1359,6 @@ describe('environment variables', () => { ), ); - await expect( - bump({ pattern: null, release: 'main' } as unknown as Command), - ).rejects.toThrow(); + await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow(); }); }); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli-module-migrate/src/commands/versions/bump.ts similarity index 87% rename from packages/cli/src/modules/migrate/commands/versions/bump.ts rename to packages/cli-module-migrate/src/commands/versions/bump.ts index ab65f8c604..518126a3de 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli-module-migrate/src/commands/versions/bump.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BACKSTAGE_JSON, bootstrapEnvProxyAgents } from '@backstage/cli-common'; +import { + BACKSTAGE_JSON, + bootstrapEnvProxyAgents, + targetPaths, +} from '@backstage/cli-common'; bootstrapEnvProxyAgents(); @@ -22,18 +26,20 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import { minimatch } from 'minimatch'; import semver from 'semver'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; + +import { + hasBackstageYarnPlugin, + Lockfile, + runConcurrentTasks, +} from '@backstage/cli-node'; import { fetchPackageInfo, - Lockfile, mapDependencies, YarnInfoInspectData, -} from '../../../../lib/versioning'; -import { runParallelWorkers } from '../../../../lib/parallel'; +} from '../../lib/versioning/packages'; import { getManifestByReleaseLine, getManifestByVersion, @@ -42,6 +48,7 @@ import { import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; const DEP_TYPES = [ 'dependencies', @@ -67,12 +74,42 @@ function extendsDefaultPattern(pattern: string): boolean { return minimatch('@backstage/', pattern.slice(0, -1)); } -export default async (opts: OptionValues) => { - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); - const hasYarnPlugin = await getHasYarnPlugin(); +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { pattern: patternFlag, release, skipInstall, skipMigrate }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + pattern: { + type: String, + description: 'Override glob for matching packages to upgrade', + }, + release: { + type: String, + description: 'Bump to a specific Backstage release line or version', + default: 'main', + }, + skipInstall: { + type: Boolean, + description: 'Skips yarn install step', + }, + skipMigrate: { + type: Boolean, + description: 'Skips migration of any moved packages', + }, + }, + }, + undefined, + args, + ); - let pattern = opts.pattern; + const lockfilePath = targetPaths.resolveRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); + + let pattern = patternFlag; if (!pattern) { console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`); @@ -91,15 +128,15 @@ export default async (opts: OptionValues) => { findTargetVersion = createStrictVersionFinder({ releaseManifest, }); - } else if (semver.valid(opts.release)) { + } else if (semver.valid(release)) { // Specific release specified. Be strict when resolving versions - releaseManifest = await getManifestByVersion({ version: opts.release }); + releaseManifest = await getManifestByVersion({ version: release! }); findTargetVersion = createStrictVersionFinder({ releaseManifest, }); } else { // Release line specified. Be lenient when resolving versions. - if (opts.release === 'next') { + if (release === 'next') { const next = await getManifestByReleaseLine({ releaseLine: 'next', versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL, @@ -114,17 +151,17 @@ export default async (opts: OptionValues) => { : main; } else { releaseManifest = await getManifestByReleaseLine({ - releaseLine: opts.release, + releaseLine: release!, versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL, }); } findTargetVersion = createVersionFinder({ - releaseLine: opts.releaseLine, + releaseLine: release, releaseManifest, }); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( `Updating yarn plugin to v${releaseManifest.releaseVersion}...`, @@ -140,13 +177,13 @@ export default async (opts: OptionValues) => { } // First we discover all Backstage dependencies within our own repo - const dependencyMap = await mapDependencies(paths.targetDir, pattern); + const dependencyMap = await mapDependencies(targetPaths.dir, pattern); // Next check with the package registry to see which dependency ranges we need to bump const versionBumps = new Map(); - await runParallelWorkers({ - parallelismFactor: 4, + await runConcurrentTasks({ + concurrencyFactor: 4, items: dependencyMap.entries(), async worker([name, pkgs]) { let target: string; @@ -182,8 +219,8 @@ export default async (opts: OptionValues) => { console.log(); const breakingUpdates = new Map(); - await runParallelWorkers({ - parallelismFactor: 4, + await runConcurrentTasks({ + concurrencyFactor: 4, items: versionBumps.entries(), async worker([name, deps]) { const pkgPath = resolvePath(deps[0].location, 'package.json'); @@ -208,7 +245,7 @@ export default async (opts: OptionValues) => { const oldLockfileRange = await asLockfileVersion(oldRange); const useBackstageRange = - hasYarnPlugin && + yarnPluginEnabled && // Only use backstage:^ versions if the package is present in // the manifest for the release we're bumping to. releaseManifest.packages.find( @@ -248,7 +285,7 @@ export default async (opts: OptionValues) => { if (extendsDefaultPattern(pattern)) { await bumpBackstageJsonVersion( releaseManifest.releaseVersion, - hasYarnPlugin, + yarnPluginEnabled, ); } else { console.log( @@ -258,7 +295,7 @@ export default async (opts: OptionValues) => { ); } - if (!opts.skipInstall) { + if (!skipInstall) { await runYarnInstall(); } else { console.log(); @@ -266,14 +303,14 @@ export default async (opts: OptionValues) => { console.log(chalk.yellow(`Skipping yarn install`)); } - if (!opts.skipMigrate) { + if (!skipMigrate) { console.log(); const changed = await migrateMovedPackages({ - pattern: opts.pattern, + pattern: patternFlag, }); - if (changed && !opts.skipInstall) { + if (changed && !skipInstall) { await runYarnInstall(); } } @@ -313,7 +350,7 @@ export default async (opts: OptionValues) => { console.log(); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( chalk.blue( @@ -417,7 +454,7 @@ export function createVersionFinder(options: { } function getBackstageJsonPath() { - return paths.resolveTargetRoot(BACKSTAGE_JSON); + return targetPaths.resolveRoot(BACKSTAGE_JSON); } async function getBackstageJson() { diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli-module-migrate/src/commands/versions/migrate.test.ts similarity index 94% rename from packages/cli/src/modules/migrate/commands/versions/migrate.test.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.test.ts index 936d10e24c..606fa528b1 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli-module-migrate/src/commands/versions/migrate.test.ts @@ -18,6 +18,7 @@ import { createMockDirectory, } from '@backstage/backend-test-utils'; import * as runObj from '@backstage/cli-common'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import migrate from './migrate'; import { withLogCollector } from '@backstage/test-utils'; import fs from 'fs-extra'; @@ -38,9 +39,7 @@ jest.mock('@backstage/cli-common', () => { return { ...actual, findPaths: () => ({ - resolveTargetRoot(filename: string) { - return mockDir.resolve(filename); - }, + resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args), get targetDir() { return mockDir.path; }, @@ -58,6 +57,7 @@ function expectLogsToMatch(receivedLogs: String[], expected: String[]): void { describe('versions:migrate', () => { mockDir = createMockDirectory(); + beforeAll(() => overrideTargetPaths(mockDir.path)); beforeEach(() => { (runObj.run as jest.Mock).mockReturnValue({ @@ -73,9 +73,7 @@ describe('versions:migrate', () => { it('should bump to the moved version when the package is moved', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -125,7 +123,7 @@ describe('versions:migrate', () => { }); const { warn, log: logs } = await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expectLogsToMatch(logs, [ @@ -177,9 +175,7 @@ describe('versions:migrate', () => { it('should replace the occurrences of the moved package in files inside the correct package', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -233,7 +229,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); @@ -264,9 +260,7 @@ describe('versions:migrate', () => { it('should replace occurrences of changed packages, and is careful', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -317,7 +311,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', name: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.ts b/packages/cli-module-migrate/src/commands/versions/migrate.ts similarity index 87% rename from packages/cli/src/modules/migrate/commands/versions/migrate.ts rename to packages/cli-module-migrate/src/commands/versions/migrate.ts index e5d4f1b2d3..a8ecb6cad4 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.ts +++ b/packages/cli-module-migrate/src/commands/versions/migrate.ts @@ -16,11 +16,12 @@ import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; import chalk from 'chalk'; import { resolve as resolvePath, join as joinPath } from 'node:path'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { readJson, writeJson } from 'fs-extra'; import { minimatch } from 'minimatch'; import { runYarnInstall } from '../../lib/utils'; import replace from 'replace-in-file'; +import type { CliCommandContext } from '@backstage/cli-node'; declare module 'replace-in-file' { export default function (config: { @@ -38,10 +39,31 @@ declare module 'replace-in-file' { >; } -export default async (options: OptionValues) => { +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { pattern, skipCodeChanges }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + pattern: { + type: String, + description: 'Override glob for matching packages to upgrade', + }, + skipCodeChanges: { + type: Boolean, + description: 'Skip code changes and only update package.json files', + }, + }, + }, + undefined, + args, + ); + const changed = await migrateMovedPackages({ - pattern: options.pattern, - skipCodeChanges: options.skipCodeChanges, + pattern, + skipCodeChanges, }); if (changed) { diff --git a/packages/cli-module-migrate/src/index.ts b/packages/cli-module-migrate/src/index.ts new file mode 100644 index 0000000000..e5d89383b8 --- /dev/null +++ b/packages/cli-module-migrate/src/index.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2024 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['versions:migrate'], + description: + 'Migrate any plugins that have been moved to the @backstage-community namespace automatically', + execute: { loader: () => import('./commands/versions/migrate') }, + }); + + reg.addCommand({ + path: ['versions:bump'], + description: 'Bump Backstage packages to the latest versions', + execute: { loader: () => import('./commands/versions/bump') }, + }); + + reg.addCommand({ + path: ['migrate', 'package-roles'], + description: `Add package role field to packages that don't have it`, + execute: { + loader: () => import('./commands/packageRole'), + }, + }); + + reg.addCommand({ + path: ['migrate', 'package-scripts'], + description: 'Set package scripts according to each package role', + execute: { + loader: () => import('./commands/packageScripts'), + }, + }); + + reg.addCommand({ + path: ['migrate', 'package-exports'], + description: 'Synchronize package subpath export definitions', + execute: { + loader: () => import('./commands/packageExports'), + }, + }); + + reg.addCommand({ + path: ['migrate', 'package-lint-configs'], + description: + 'Migrates all packages to use @backstage/cli/config/eslint-factory', + execute: { + loader: () => import('./commands/packageLintConfigs'), + }, + }); + + reg.addCommand({ + path: ['migrate', 'react-router-deps'], + description: + 'Migrates the react-router dependencies for all packages to be peer dependencies', + execute: { + loader: () => import('./commands/reactRouterDeps'), + }, + }); + }, +}); diff --git a/packages/cli/src/modules/migrate/lib/utils.ts b/packages/cli-module-migrate/src/lib/utils.ts similarity index 100% rename from packages/cli/src/modules/migrate/lib/utils.ts rename to packages/cli-module-migrate/src/lib/utils.ts diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli-module-migrate/src/lib/versioning/packages.test.ts similarity index 98% rename from packages/cli/src/lib/versioning/packages.test.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.test.ts index e823ce8769..8946b8ba2f 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli-module-migrate/src/lib/versioning/packages.test.ts @@ -104,9 +104,7 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['pkgs/*'], - }, + workspaces: ['pkgs/*'], }), pkgs: { a: { diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli-module-migrate/src/lib/versioning/packages.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.ts rename to packages/cli-module-migrate/src/lib/versioning/packages.ts diff --git a/packages/cli/src/lib/versioning/yarn.ts b/packages/cli-module-migrate/src/lib/versioning/yarn.ts similarity index 100% rename from packages/cli/src/lib/versioning/yarn.ts rename to packages/cli-module-migrate/src/lib/versioning/yarn.ts diff --git a/packages/cli-module-new/.eslintrc.js b/packages/cli-module-new/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-new/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-new/README.md b/packages/cli-module-new/README.md new file mode 100644 index 0000000000..88855e856a --- /dev/null +++ b/packages/cli-module-new/README.md @@ -0,0 +1,14 @@ +# @backstage/cli-module-new + +CLI module that provides the `new` command for the Backstage CLI, offering an interactive guide to scaffold new plugins, packages, and modules in your Backstage app. + +## Commands + +| Command | Description | +| :------ | :-------------------------------------------------------------- | +| `new` | Open up an interactive guide to creating new things in your app | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-new/bin/backstage-cli-module-new b/packages/cli-module-new/bin/backstage-cli-module-new new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-new/bin/backstage-cli-module-new @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-new/catalog-info.yaml b/packages/cli-module-new/catalog-info.yaml new file mode 100644 index 0000000000..997f06d629 --- /dev/null +++ b/packages/cli-module-new/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-new + title: '@backstage/cli-module-new' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-new/cli-report.md b/packages/cli-module-new/cli-report.md new file mode 100644 index 0000000000..a2b4768148 --- /dev/null +++ b/packages/cli-module-new/cli-report.md @@ -0,0 +1,34 @@ +## CLI Report file for "@backstage/cli-module-new" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-new` + +``` +Usage: @backstage/cli-module-new [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + new +``` + +### `backstage-cli-module-new new` + +``` +Usage: @backstage/cli-module-new new + +Options: + --base-version + --license + --npm-registry + --option + --private + --scope + --select + --skip-install + -h, --help +``` diff --git a/packages/cli-module-new/package.json b/packages/cli-module-new/package.json new file mode 100644 index 0000000000..9ca1664f7a --- /dev/null +++ b/packages/cli-module-new/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/cli-module-new", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-new" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", + "chalk": "^4.0.0", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "handlebars": "^4.7.3", + "inquirer": "^8.2.0", + "lodash": "^4.17.21", + "ora": "^5.3.0", + "recursive-readdir": "^2.2.2", + "semver": "^7.5.3", + "yaml": "^2.0.0", + "zod": "^3.25.76", + "zod-validation-error": "^4.0.2" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@types/fs-extra": "^11.0.0", + "@types/inquirer": "^8.1.3", + "@types/lodash": "^4.14.151", + "@types/recursive-readdir": "^2.2.0" + }, + "bin": "bin/backstage-cli-module-new" +} diff --git a/packages/cli-module-new/report.api.md b/packages/cli-module-new/report.api.md new file mode 100644 index 0000000000..0a9924e731 --- /dev/null +++ b/packages/cli-module-new/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-new" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli-module-new/src/commands/new.test.ts similarity index 80% rename from packages/cli/src/modules/new/commands/new.test.ts rename to packages/cli-module-new/src/commands/new.test.ts index 2183d80062..54663ab60d 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli-module-new/src/commands/new.test.ts @@ -16,6 +16,7 @@ import { createNewPackage } from '../lib/createNewPackage'; import { default as newCommand } from './new'; +import type { CliCommandContext } from '@backstage/cli-node'; jest.mock('../lib/createNewPackage'); @@ -34,13 +35,21 @@ describe.each([ }); it(`should generate naming options for --scope=${scope}`, async () => { - await newCommand({ scope, option: [], skipInstall: false }); + const args = ['--skip-install']; + if (scope) { + args.push('--scope', scope); + } + const context: CliCommandContext = { + args, + info: { usage: 'backstage-cli new', name: 'new' }, + }; + await newCommand(context); expect(createNewPackage).toHaveBeenCalledWith( expect.objectContaining({ - configOverrides: { + configOverrides: expect.objectContaining({ packageNamePrefix: prefix, packageNamePluginInfix: infix, - }, + }), }), ); }); diff --git a/packages/cli-module-new/src/commands/new.ts b/packages/cli-module-new/src/commands/new.ts new file mode 100644 index 0000000000..3d7fcdf4e3 --- /dev/null +++ b/packages/cli-module-new/src/commands/new.ts @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { createNewPackage } from '../lib/createNewPackage'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + for (const flag of ['skipInstall', 'npmRegistry', 'baseVersion']) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } + } + + const { + flags: { + select, + option: rawArgOptions, + skipInstall, + scope, + npmRegistry, + baseVersion, + license, + private: isPrivate, + }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + select: { + type: String, + description: 'Select the thing you want to be creating upfront', + }, + option: { + type: [String] as const, + description: 'Pre-fill options for the creation process', + default: [] as string[], + }, + skipInstall: { + type: Boolean, + description: `Skips running 'yarn install' and 'yarn lint --fix'`, + }, + scope: { + type: String, + description: 'The scope to use for new packages', + }, + npmRegistry: { + type: String, + description: 'The package registry to use for new packages', + }, + baseVersion: { + type: String, + description: + 'The version to use for any new packages (default: 0.1.0)', + }, + license: { + type: String, + description: + 'The license to use for any new packages (default: Apache-2.0)', + }, + private: { + type: Boolean, + description: 'Mark new packages as private', + default: true, + }, + }, + }, + undefined, + args, + ); + + const prefilledParams = parseParams(rawArgOptions); + + let pluginInfix: string | undefined = undefined; + let packagePrefix: string | undefined = undefined; + if (scope) { + const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`; + packagePrefix = normalizedScope.includes('/') + ? normalizedScope + : `${normalizedScope}/`; + pluginInfix = scope.includes('backstage') ? 'plugin-' : 'backstage-plugin-'; + } + + if ( + isPrivate === false || + [npmRegistry, baseVersion, license].filter(Boolean).length !== 0 + ) { + console.warn( + `Global template configuration via CLI flags is deprecated, see https://backstage.io/docs/cli/new for information on how to configure package templating`, + ); + } + + await createNewPackage({ + prefilledParams, + preselectedTemplateId: select, + configOverrides: { + license, + version: baseVersion, + private: isPrivate, + publishRegistry: npmRegistry, + packageNamePrefix: packagePrefix, + packageNamePluginInfix: pluginInfix, + }, + skipInstall: Boolean(skipInstall), + }); +}; + +function parseParams(optionStrings: string[]): Record { + const options: Record = {}; + + for (const str of optionStrings) { + const [key] = str.split('=', 1); + const value = str.slice(key.length + 1); + if (!key || str[key.length] !== '=') { + throw new Error( + `Invalid option '${str}', must be of the format =`, + ); + } + options[key] = value; + } + + return options; +} diff --git a/packages/cli-module-new/src/index.ts b/packages/cli-module-new/src/index.ts new file mode 100644 index 0000000000..c9bc322083 --- /dev/null +++ b/packages/cli-module-new/src/index.ts @@ -0,0 +1,51 @@ +/* + * 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 { createCliModule } from '@backstage/cli-node'; +import { NotImplementedError } from '@backstage/errors'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['new'], + description: + 'Open up an interactive guide to creating new things in your app', + execute: { loader: () => import('./commands/new') }, + }); + + reg.addCommand({ + path: ['create'], + description: 'Create a new Backstage app', + deprecated: true, + execute: async () => { + throw new NotImplementedError( + `This command has been removed, use 'backstage-cli new' instead`, + ); + }, + }); + reg.addCommand({ + path: ['create-plugin'], + description: 'Create a new Backstage plugin', + deprecated: true, + execute: async () => { + throw new NotImplementedError( + `This command has been removed, use 'backstage-cli new' instead`, + ); + }, + }); + }, +}); diff --git a/packages/cli/src/modules/new/lib/codeowners/codeowners.test.ts b/packages/cli-module-new/src/lib/codeowners/codeowners.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/codeowners/codeowners.test.ts rename to packages/cli-module-new/src/lib/codeowners/codeowners.test.ts diff --git a/packages/cli/src/modules/new/lib/codeowners/codeowners.ts b/packages/cli-module-new/src/lib/codeowners/codeowners.ts similarity index 96% rename from packages/cli/src/modules/new/lib/codeowners/codeowners.ts rename to packages/cli-module-new/src/lib/codeowners/codeowners.ts index 1cfddabffb..c0cddba126 100644 --- a/packages/cli/src/modules/new/lib/codeowners/codeowners.ts +++ b/packages/cli-module-new/src/lib/codeowners/codeowners.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import path from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/; const USER_ID_RE = /^@[-\w]+$/; @@ -82,7 +82,7 @@ export async function addCodeownersEntry( let filePath = codeownersFilePath; if (!filePath) { - filePath = await getCodeownersFilePath(paths.targetRoot); + filePath = await getCodeownersFilePath(targetPaths.rootDir); if (!filePath) { return false; } diff --git a/packages/cli/src/modules/new/lib/codeowners/index.ts b/packages/cli-module-new/src/lib/codeowners/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/codeowners/index.ts rename to packages/cli-module-new/src/lib/codeowners/index.ts diff --git a/packages/cli/src/modules/new/lib/createNewPackage.ts b/packages/cli-module-new/src/lib/createNewPackage.ts similarity index 100% rename from packages/cli/src/modules/new/lib/createNewPackage.ts rename to packages/cli-module-new/src/lib/createNewPackage.ts diff --git a/packages/cli/src/modules/new/lib/defaultTemplates.ts b/packages/cli-module-new/src/lib/defaultTemplates.ts similarity index 100% rename from packages/cli/src/modules/new/lib/defaultTemplates.ts rename to packages/cli-module-new/src/lib/defaultTemplates.ts diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli-module-new/src/lib/execution/PortableTemplater.ts similarity index 87% rename from packages/cli/src/modules/new/lib/execution/PortableTemplater.ts rename to packages/cli-module-new/src/lib/execution/PortableTemplater.ts index 9dfd60baba..dd6f43b9a7 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli-module-new/src/lib/execution/PortableTemplater.ts @@ -24,10 +24,11 @@ import startCase from 'lodash/startCase'; import upperCase from 'lodash/upperCase'; import upperFirst from 'lodash/upperFirst'; import lowerFirst from 'lodash/lowerFirst'; -import { Lockfile } from '../../../../lib/versioning'; -import { paths } from '../../../../lib/paths'; -import { createPackageVersionProvider } from '../../../../lib/version'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; +import { Lockfile } from '@backstage/cli-node'; +import { targetPaths } from '@backstage/cli-common'; + +import { createPackageVersionProvider } from '../version'; +import { hasBackstageYarnPlugin } from '@backstage/cli-node'; const builtInHelpers = { camelCase, @@ -49,14 +50,14 @@ export class PortableTemplater { static async create(options: CreatePortableTemplaterOptions = {}) { let lockfile: Lockfile | undefined; try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + lockfile = await Lockfile.load(targetPaths.resolveRoot('yarn.lock')); } catch { /* ignored */ } - const hasYarnPlugin = await getHasYarnPlugin(); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); const versionProvider = createPackageVersionProvider(lockfile, { - preferBackstageProtocol: hasYarnPlugin, + preferBackstageProtocol: yarnPluginEnabled, }); const templater = new PortableTemplater( diff --git a/packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts b/packages/cli-module-new/src/lib/execution/executePortableTemplate.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/executePortableTemplate.ts rename to packages/cli-module-new/src/lib/execution/executePortableTemplate.ts diff --git a/packages/cli/src/modules/new/lib/execution/index.ts b/packages/cli-module-new/src/lib/execution/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/execution/index.ts rename to packages/cli-module-new/src/lib/execution/index.ts diff --git a/packages/cli/src/modules/new/lib/execution/installNewPackage.ts b/packages/cli-module-new/src/lib/execution/installNewPackage.ts similarity index 95% rename from packages/cli/src/modules/new/lib/execution/installNewPackage.ts rename to packages/cli-module-new/src/lib/execution/installNewPackage.ts index 0fe0284386..4bbb3730df 100644 --- a/packages/cli/src/modules/new/lib/execution/installNewPackage.ts +++ b/packages/cli-module-new/src/lib/execution/installNewPackage.ts @@ -16,7 +16,8 @@ import fs from 'fs-extra'; import upperFirst from 'lodash/upperFirst'; import camelCase from 'lodash/camelCase'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { Task } from '../tasks'; import { PortableTemplateInput } from '../types'; @@ -52,7 +53,7 @@ export async function installNewPackage(input: PortableTemplateInput) { } async function addDependency(input: PortableTemplateInput, path: string) { - const pkgJsonPath = paths.resolveTargetRoot(path); + const pkgJsonPath = targetPaths.resolveRoot(path); const pkgJson = await fs.readJson(pkgJsonPath).catch(error => { if (error.code === 'ENOENT') { @@ -84,7 +85,7 @@ async function tryAddFrontendLegacy(input: PortableTemplateInput) { ); } - const appDefinitionPath = paths.resolveTargetRoot('packages/app/src/App.tsx'); + const appDefinitionPath = targetPaths.resolveRoot('packages/app/src/App.tsx'); if (!(await fs.pathExists(appDefinitionPath))) { return; } @@ -120,7 +121,7 @@ async function tryAddFrontendLegacy(input: PortableTemplateInput) { } async function tryAddBackend(input: PortableTemplateInput) { - const backendIndexPath = paths.resolveTargetRoot( + const backendIndexPath = targetPaths.resolveRoot( 'packages/backend/src/index.ts', ); if (!(await fs.pathExists(backendIndexPath))) { diff --git a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts b/packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts similarity index 80% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts index a9f1ad61f7..8132246be6 100644 --- a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.test.ts +++ b/packages/cli-module-new/src/lib/execution/writeTemplateContents.test.ts @@ -17,7 +17,10 @@ import { relative as relativePath } from 'node:path'; import { writeTemplateContents } from './writeTemplateContents'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { paths } from '../../../../lib/paths'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; + +const mockDir = createMockDirectory(); +overrideTargetPaths(mockDir.path); const baseConfig = { version: '0.1.0', @@ -26,14 +29,14 @@ const baseConfig = { }; describe('writeTemplateContents', () => { - const mockDir = createMockDirectory(); - beforeEach(() => { mockDir.clear(); + mockDir.setContent({ + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), + }); jest.resetAllMocks(); - jest - .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...args) => mockDir.resolve(...args)); }); it('should write an empty template', async () => { @@ -53,7 +56,11 @@ describe('writeTemplateContents', () => { ); expect(relativePath(mockDir.path, targetDir)).toBe('plugins/plugin-test'); - expect(mockDir.content()).toEqual({}); + expect(mockDir.content()).toEqual({ + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), + }); }); it('should write template with various files', async () => { @@ -87,6 +94,9 @@ describe('writeTemplateContents', () => { ); expect(mockDir.content()).toEqual({ + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), out: { 'test.txt': 'test', 'plugin.txt': 'id=test', diff --git a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts b/packages/cli-module-new/src/lib/execution/writeTemplateContents.ts similarity index 96% rename from packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts rename to packages/cli-module-new/src/lib/execution/writeTemplateContents.ts index 8b2f9ebf1e..619a6fc3f2 100644 --- a/packages/cli/src/modules/new/lib/execution/writeTemplateContents.ts +++ b/packages/cli-module-new/src/lib/execution/writeTemplateContents.ts @@ -17,18 +17,17 @@ import fs from 'fs-extra'; import { dirname, resolve as resolvePath } from 'node:path'; -import { paths } from '../../../../lib/paths'; import { PortableTemplate, PortableTemplateInput } from '../types'; import { ForwardedError, InputError } from '@backstage/errors'; import { isMonoRepo as getIsMonoRepo } from '@backstage/cli-node'; import { PortableTemplater } from './PortableTemplater'; -import { isChildPath } from '@backstage/cli-common'; +import { isChildPath, targetPaths } from '@backstage/cli-common'; export async function writeTemplateContents( template: PortableTemplate, input: PortableTemplateInput, ): Promise<{ targetDir: string }> { - const targetDir = paths.resolveTargetRoot(input.packagePath); + const targetDir = targetPaths.resolveRoot(input.packagePath); if (await fs.pathExists(targetDir)) { throw new InputError(`Package '${input.packagePath}' already exists`); diff --git a/packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.test.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.test.ts rename to packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts similarity index 97% rename from packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts rename to packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts index 38e8eaddba..7279801d62 100644 --- a/packages/cli/src/modules/new/lib/preparation/collectPortableTemplateInput.ts +++ b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts @@ -16,7 +16,8 @@ import inquirer, { DistinctQuestion } from 'inquirer'; import { getCodeownersFilePath, parseOwnerIds } from '../codeowners'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { PortableTemplateConfig, PortableTemplateInput, @@ -38,7 +39,7 @@ export async function collectPortableTemplateInput( ): Promise { const { config, template, prefilledParams } = options; - const codeOwnersFilePath = await getCodeownersFilePath(paths.targetRoot); + const codeOwnersFilePath = await getCodeownersFilePath(targetPaths.rootDir); const prompts = getPromptsForRole(template.role); diff --git a/packages/cli/src/modules/new/lib/preparation/index.ts b/packages/cli-module-new/src/lib/preparation/index.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/index.ts rename to packages/cli-module-new/src/lib/preparation/index.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.test.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplate.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts similarity index 96% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts index f7053df552..6a683fb31c 100644 --- a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplate.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts @@ -20,7 +20,8 @@ import recursiveReaddir from 'recursive-readdir'; import { resolve as resolvePath, relative as relativePath } from 'node:path'; import { dirname } from 'node:path'; import { parse as parseYaml } from 'yaml'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { PortableTemplateFile, PortableTemplatePointer, @@ -46,7 +47,7 @@ export async function loadPortableTemplate( throw new Error('Remote templates are not supported yet'); } const templateContent = await fs - .readFile(paths.resolveTargetRoot(pointer.target), 'utf-8') + .readFile(targetPaths.resolveRoot(pointer.target), 'utf-8') .catch(error => { throw new ForwardedError( `Failed to load template definition from '${pointer.target}'`, diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.test.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts similarity index 97% rename from packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts rename to packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts index dd937482a2..5040904138 100644 --- a/packages/cli/src/modules/new/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts @@ -16,7 +16,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath, dirname, isAbsolute } from 'node:path'; -import { paths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; + import { defaultTemplates } from '../defaultTemplates'; import { PortableTemplateConfig, @@ -90,7 +91,7 @@ export async function loadPortableTemplateConfig( ): Promise { const { overrides = {} } = options; const pkgPath = - options.packagePath ?? paths.resolveTargetRoot('package.json'); + options.packagePath ?? targetPaths.resolveRoot('package.json'); const pkgJson = await fs.readJson(pkgPath); const parsed = pkgJsonWithNewConfigSchema.safeParse(pkgJson); diff --git a/packages/cli/src/modules/new/lib/preparation/resolvePackageParams.test.ts b/packages/cli-module-new/src/lib/preparation/resolvePackageParams.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/resolvePackageParams.test.ts rename to packages/cli-module-new/src/lib/preparation/resolvePackageParams.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/resolvePackageParams.ts b/packages/cli-module-new/src/lib/preparation/resolvePackageParams.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/resolvePackageParams.ts rename to packages/cli-module-new/src/lib/preparation/resolvePackageParams.ts diff --git a/packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.test.ts b/packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.test.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.test.ts rename to packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.test.ts diff --git a/packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.ts b/packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.ts similarity index 100% rename from packages/cli/src/modules/new/lib/preparation/selectTemplateInteractively.ts rename to packages/cli-module-new/src/lib/preparation/selectTemplateInteractively.ts diff --git a/packages/cli/src/modules/new/lib/tasks.ts b/packages/cli-module-new/src/lib/tasks.ts similarity index 100% rename from packages/cli/src/modules/new/lib/tasks.ts rename to packages/cli-module-new/src/lib/tasks.ts diff --git a/packages/cli/src/modules/new/lib/types.ts b/packages/cli-module-new/src/lib/types.ts similarity index 100% rename from packages/cli/src/modules/new/lib/types.ts rename to packages/cli-module-new/src/lib/types.ts diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli-module-new/src/lib/version.test.ts similarity index 97% rename from packages/cli/src/lib/version.test.ts rename to packages/cli-module-new/src/lib/version.test.ts index d582c397c3..829a2f3d9e 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli-module-new/src/lib/version.test.ts @@ -15,7 +15,7 @@ */ import { packageVersions, createPackageVersionProvider } from './version'; -import { Lockfile } from './versioning'; +import { Lockfile } from '@backstage/cli-node'; import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -78,6 +78,9 @@ describe('createPackageVersionProvider', () => { expect(provider('@backstage/core-plugin-api')).toBe( `^${corePluginApiPkg.version}`, ); + expect(provider('@backstage/frontend-dev-utils')).toBe( + `^${packageVersions['@backstage/frontend-dev-utils']}`, + ); }); describe('with backstage protocol options', () => { diff --git a/packages/cli/src/lib/version.ts b/packages/cli-module-new/src/lib/version.ts similarity index 69% rename from packages/cli/src/lib/version.ts rename to packages/cli-module-new/src/lib/version.ts index a7a7ca1bdb..27e63bafdf 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli-module-new/src/lib/version.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import fs from 'fs-extra'; import semver from 'semver'; -import { paths } from './paths'; -import { Lockfile } from './versioning'; +import { Lockfile } from '@backstage/cli-node'; /* eslint-disable @backstage/no-relative-monorepo-imports */ /* @@ -32,28 +30,30 @@ This does not create an actual dependency on these packages and does not bring i Rollup will extract the value of the version field in each package at build time without leaving any imports in place. */ -import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json'; -import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json'; -import { version as catalogClient } from '../../../../packages/catalog-client/package.json'; -import { version as cli } from '../../../../packages/cli/package.json'; -import { version as config } from '../../../../packages/config/package.json'; -import { version as coreAppApi } from '../../../../packages/core-app-api/package.json'; -import { version as coreComponents } from '../../../../packages/core-components/package.json'; -import { version as corePluginApi } from '../../../../packages/core-plugin-api/package.json'; -import { version as devUtils } from '../../../../packages/dev-utils/package.json'; -import { version as errors } from '../../../../packages/errors/package.json'; -import { version as frontendDefaults } from '../../../../packages/frontend-defaults/package.json'; -import { version as frontendPluginApi } from '../../../../packages/frontend-plugin-api/package.json'; -import { version as frontendTestUtils } from '../../../../packages/frontend-test-utils/package.json'; -import { version as testUtils } from '../../../../packages/test-utils/package.json'; -import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json'; -import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json'; + +import { version as backendDefaults } from '../../../backend-defaults/package.json'; +import { version as backendPluginApi } from '../../../backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../backend-test-utils/package.json'; +import { version as catalogClient } from '../../../catalog-client/package.json'; +import { version as cli } from '../../../cli/package.json'; +import { version as config } from '../../../config/package.json'; +import { version as coreAppApi } from '../../../core-app-api/package.json'; +import { version as coreComponents } from '../../../core-components/package.json'; +import { version as corePluginApi } from '../../../core-plugin-api/package.json'; +import { version as devUtils } from '../../../dev-utils/package.json'; +import { version as errors } from '../../../errors/package.json'; +import { version as frontendDevUtils } from '../../../frontend-dev-utils/package.json'; +import { version as frontendDefaults } from '../../../frontend-defaults/package.json'; +import { version as frontendPluginApi } from '../../../frontend-plugin-api/package.json'; +import { version as frontendTestUtils } from '../../../frontend-test-utils/package.json'; +import { version as testUtils } from '../../../test-utils/package.json'; +import { version as theme } from '../../../theme/package.json'; +import { version as types } from '../../../types/package.json'; import { version as authBackend } from '../../../../plugins/auth-backend/package.json'; import { version as authBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; import { version as catalogNode } from '../../../../plugins/catalog-node/package.json'; -import { version as theme } from '../../../../packages/theme/package.json'; -import { version as types } from '../../../../packages/types/package.json'; -import { version as backendDefaults } from '../../../../packages/backend-defaults/package.json'; +import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json'; +import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json'; export const packageVersions: Record = { '@backstage/backend-defaults': backendDefaults, @@ -67,6 +67,7 @@ export const packageVersions: Record = { '@backstage/core-plugin-api': corePluginApi, '@backstage/dev-utils': devUtils, '@backstage/errors': errors, + '@backstage/frontend-dev-utils': frontendDevUtils, '@backstage/frontend-defaults': frontendDefaults, '@backstage/frontend-plugin-api': frontendPluginApi, '@backstage/frontend-test-utils': frontendTestUtils, @@ -81,14 +82,6 @@ export const packageVersions: Record = { '@backstage/plugin-catalog-node': catalogNode, }; -export function findVersion() { - const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8'); - return JSON.parse(pkgContent).version; -} - -export const version = findVersion(); -export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); - export function createPackageVersionProvider( lockfile?: Lockfile, options?: { diff --git a/packages/cli-module-test-jest/.eslintrc.js b/packages/cli-module-test-jest/.eslintrc.js new file mode 100644 index 0000000000..6f313a7a5e --- /dev/null +++ b/packages/cli-module-test-jest/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + ignorePatterns: ['config/**'], +}); diff --git a/packages/cli-module-test-jest/README.md b/packages/cli-module-test-jest/README.md new file mode 100644 index 0000000000..e56ffea2a7 --- /dev/null +++ b/packages/cli-module-test-jest/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-test-jest + +CLI module that provides Jest-based testing commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :------------- | :---------------------------------------------------------------- | +| `package test` | Run tests, forwarding arguments to Jest, defaulting to watch mode | +| `repo test` | Run tests, forwarding arguments to Jest, defaulting to watch mode | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-test-jest/bin/backstage-cli-module-test-jest @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-test-jest/catalog-info.yaml b/packages/cli-module-test-jest/catalog-info.yaml new file mode 100644 index 0000000000..c928c628f5 --- /dev/null +++ b/packages/cli-module-test-jest/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-test-jest + title: '@backstage/cli-module-test-jest' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-test-jest/cli-report.md b/packages/cli-module-test-jest/cli-report.md new file mode 100644 index 0000000000..00523eb8cf --- /dev/null +++ b/packages/cli-module-test-jest/cli-report.md @@ -0,0 +1,170 @@ +## CLI Report file for "@backstage/cli-module-test-jest" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-test-jest` + +``` +Usage: @backstage/cli-module-test-jest [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + package [command] + repo [command] +``` + +### `backstage-cli-module-test-jest package` + +``` +Usage: @backstage/cli-module-test-jest package [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `backstage-cli-module-test-jest package test` + +``` +Usage: backstage-cli-module-test-jest [--config=] + +Options: + --all + --automock + --cache + --cacheDirectory + --changedFilesWithAncestor + --changedSince + --ci + --clearCache + --clearMocks + --collectCoverage + --collectCoverageFrom + --color + --colors + --coverage + --coverageDirectory + --coveragePathIgnorePatterns + --coverageProvider + --coverageReporters + --coverageThreshold + --debug + --detectLeaks + --detectOpenHandles + --errorOnDeprecated + --filter + --findRelatedTests + --forceExit + --globalSetup + --globalTeardown + --globals + --haste + --ignoreProjects + --injectGlobals + --json + --lastCommit + --listTests + --logHeapUsage + --maxConcurrency + --moduleDirectories + --moduleFileExtensions + --moduleNameMapper + --modulePathIgnorePatterns + --modulePaths + --noStackTrace + --notify + --notifyMode + --openHandlesTimeout + --outputFile + --passWithNoTests + --preset + --prettierPath + --projects + --randomize + --reporters + --resetMocks + --resetModules + --resolver + --restoreMocks + --rootDir + --roots + --runTestsByPath + --runner + --seed + --selectProjects + --setupFiles + --setupFilesAfterEnv + --shard + --showConfig + --showSeed + --silent + --skipFilter + --snapshotSerializers + --testEnvironment, --env + --testEnvironmentOptions + --testFailureExitCode + --testLocationInResults + --testMatch + --testPathIgnorePatterns + --testPathPatterns + --testRegex + --testResultsProcessor + --testRunner + --testSequencer + --testTimeout + --transform + --transformIgnorePatterns + --unmockedModulePathPatterns + --useStderr + --verbose + --version + --waitForUnhandledRejections + --watch + --watchAll + --watchPathIgnorePatterns + --watchman + --workerThreads + -b, --bail + -c, --config + -e, --expand + -f, --onlyFailures + -h, --help + -i, --runInBand + -o, --onlyChanged + -t, --testNamePattern + -u, --updateSnapshot + -w, --maxWorkers +``` + +### `backstage-cli-module-test-jest repo` + +``` +Usage: @backstage/cli-module-test-jest repo [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + test +``` + +### `backstage-cli-module-test-jest repo test` + +``` +Usage: @backstage/cli-module-test-jest repo test + +Options: + --jest-help + --since + --success-cache + --success-cache-dir + -h, --help +``` diff --git a/packages/cli-module-test-jest/config/getJestEnvironment.js b/packages/cli-module-test-jest/config/getJestEnvironment.js new file mode 100644 index 0000000000..aac2f79021 --- /dev/null +++ b/packages/cli-module-test-jest/config/getJestEnvironment.js @@ -0,0 +1,49 @@ +/* + * 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. + */ + +function getJestMajorVersion() { + const jestVersion = require('jest/package.json').version; + const majorVersion = parseInt(jestVersion.split('.')[0], 10); + return majorVersion; +} + +function getJestEnvironment() { + const majorVersion = getJestMajorVersion(); + + if (majorVersion >= 30) { + try { + require.resolve('@jest/environment-jsdom-abstract'); + require.resolve('jsdom'); + } catch { + throw new Error( + 'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' + + 'Please install them as dev dependencies.', + ); + } + return require.resolve('./jest-environment-jsdom'); + } + try { + require.resolve('jest-environment-jsdom'); + } catch { + throw new Error( + 'Jest 29 requires jest-environment-jsdom. ' + + 'Please install it as a dev dependency.', + ); + } + return require.resolve('jest-environment-jsdom'); +} + +module.exports = { getJestMajorVersion, getJestEnvironment }; diff --git a/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js b/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js new file mode 100644 index 0000000000..9d8b76eaf5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jest-environment-jsdom/index.js @@ -0,0 +1,61 @@ +/* + * 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. + */ + +const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default; +const jsdom = require('jsdom'); + +/** + * A custom JSDOM environment that extends the abstract base and applies + * fixes for Web API globals that are missing or incorrectly implemented + * in JSDOM. + * + * Based on https://github.com/mswjs/jest-fixed-jsdom + */ +class FixedJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + + // Fix Web API globals that JSDOM doesn't properly expose + this.global.TextDecoder = TextDecoder; + this.global.TextEncoder = TextEncoder; + this.global.TextDecoderStream = TextDecoderStream; + this.global.TextEncoderStream = TextEncoderStream; + this.global.ReadableStream = ReadableStream; + + this.global.Blob = Blob; + this.global.Headers = Headers; + this.global.FormData = FormData; + this.global.Request = Request; + this.global.Response = Response; + this.global.fetch = fetch; + this.global.AbortController = AbortController; + this.global.AbortSignal = AbortSignal; + this.global.structuredClone = structuredClone; + this.global.URL = URL; + this.global.URLSearchParams = URLSearchParams; + + this.global.BroadcastChannel = BroadcastChannel; + this.global.TransformStream = TransformStream; + this.global.WritableStream = WritableStream; + + // Needed to ensure `e instanceof Error` works as expected with errors thrown from + // any of the native APIs above. Without this, the JSDOM `Error` is what the test + // code will use for comparison with `e`, which fails the instanceof check. + this.global.Error = Error; + } +} + +module.exports = FixedJSDOMEnvironment; diff --git a/packages/cli-module-test-jest/config/jest.js b/packages/cli-module-test-jest/config/jest.js new file mode 100644 index 0000000000..8a88483d42 --- /dev/null +++ b/packages/cli-module-test-jest/config/jest.js @@ -0,0 +1,422 @@ +/* + * Copyright 2020 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. + */ + +const fs = require('fs-extra'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const glob = require('node:util').promisify(require('glob')); +const { version } = require('../package.json'); +const paths = require('@backstage/cli-common').findPaths(process.cwd()); +const { + getJestEnvironment, + getJestMajorVersion, +} = require('./getJestEnvironment'); + +const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs']; + +const FRONTEND_ROLES = [ + 'frontend', + 'web-library', + 'common-library', + 'frontend-plugin', + 'frontend-plugin-module', +]; + +const NODE_ROLES = [ + 'backend', + 'cli', + 'cli-module', + 'node-library', + 'backend-plugin', + 'backend-plugin-module', +]; + +const envOptions = { + oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), +}; + +try { + require.resolve('react-dom/client', { + paths: [paths.targetRoot], + }); + process.env.HAS_REACT_DOM_CLIENT = true; +} catch { + /* ignored */ +} + +/** + * A list of config keys that are valid for project-level config. + * Jest will complain if we forward any other root configuration to the projects. + * + * @type {Array} + */ +const projectConfigKeys = [ + 'automock', + 'cache', + 'cacheDirectory', + 'clearMocks', + 'collectCoverageFrom', + 'coverageDirectory', + 'coveragePathIgnorePatterns', + 'cwd', + 'dependencyExtractor', + 'detectLeaks', + 'detectOpenHandles', + 'displayName', + 'errorOnDeprecated', + 'extensionsToTreatAsEsm', + 'fakeTimers', + 'filter', + 'forceCoverageMatch', + 'globalSetup', + 'globalTeardown', + 'globals', + 'haste', + 'id', + 'injectGlobals', + 'moduleDirectories', + 'moduleFileExtensions', + 'moduleNameMapper', + 'modulePathIgnorePatterns', + 'modulePaths', + 'openHandlesTimeout', + 'preset', + 'prettierPath', + 'resetMocks', + 'resetModules', + 'resolver', + 'restoreMocks', + 'rootDir', + 'roots', + 'runner', + 'runtime', + 'sandboxInjectedGlobals', + 'setupFiles', + 'setupFilesAfterEnv', + 'skipFilter', + 'skipNodeResolution', + 'slowTestThreshold', + 'snapshotResolver', + 'snapshotSerializers', + 'snapshotFormat', + 'testEnvironment', + 'testEnvironmentOptions', + 'testMatch', + 'testLocationInResults', + 'testPathIgnorePatterns', + 'testRegex', + 'testRunner', + 'transform', + 'transformIgnorePatterns', + 'watchPathIgnorePatterns', + 'unmockedModulePathPatterns', + 'workerIdleMemoryLimit', +]; + +const transformIgnorePattern = [ + '@material-ui', + 'ajv', + 'core-js', + 'jest-.*', + 'jsdom', + 'knex', + 'react', + 'react-dom', + 'highlight\\.js', + 'prismjs', + 'json-schema', + 'react-use/lib', + 'typescript', +].join('|'); + +// Provides additional config that's based on the role of the target package +function getRoleConfig(role, pkgJson) { + // Only Node.js package roles support native ESM modules, frontend and common + // packages are always transpiled to CommonJS. + const moduleOpts = NODE_ROLES.includes(role) + ? { + module: { + ignoreDynamic: true, + exportInteropAnnotation: true, + }, + } + : undefined; + + const transform = { + '\\.(mjs|cjs|js)$': [ + require.resolve('./jestSwcTransform'), + { + ...moduleOpts, + jsc: { + parser: { + syntax: 'ecmascript', + }, + }, + }, + ], + '\\.jsx$': [ + require.resolve('./jestSwcTransform'), + { + jsc: { + parser: { + syntax: 'ecmascript', + jsx: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + '\\.(mts|cts|ts)$': [ + require.resolve('./jestSwcTransform'), + { + ...moduleOpts, + jsc: { + parser: { + syntax: 'typescript', + }, + }, + }, + ], + '\\.tsx$': [ + require.resolve('./jestSwcTransform'), + { + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + '\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$': + require.resolve('./jestFileTransform.js'), + '\\.(yaml)$': require.resolve('./jestYamlTransform'), + }; + if (FRONTEND_ROLES.includes(role)) { + return { + testEnvironment: getJestEnvironment(), + // The caching module loader is only used to speed up frontend tests, + // as it breaks real dynamic imports of ESM modules. + runtime: envOptions.oldTests + ? undefined + : require.resolve('./jestCachingModuleLoader'), + transform, + }; + } + return { + testEnvironment: require.resolve('jest-environment-node'), + moduleFileExtensions: [...SRC_EXTS, 'json', 'node'], + // Jest doesn't let us dynamically detect type=module per transformed file, + // so we have to assume that if the entry point is ESM, all TS files are + // ESM. + // + // This means you can't switch a package to type=module until all of its + // monorepo dependencies are also type=module or does not contain any .ts + // files. + extensionsToTreatAsEsm: + pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'], + transform, + }; +} + +async function getProjectConfig(targetPath, extraConfig, extraOptions) { + const configJsPath = path.resolve(targetPath, 'jest.config.js'); + const configTsPath = path.resolve(targetPath, 'jest.config.ts'); + // If the package has it's own jest config, we use that instead. + if (await fs.pathExists(configJsPath)) { + return require(configJsPath); + } else if (await fs.pathExists(configTsPath)) { + return require(configTsPath); + } + + // Jest config can be defined both in the root package.json and within each package. The root config + // gets forwarded to us through the `extraConfig` parameter, while the package config is read here. + // If they happen to be the same the keys will simply override each other. + // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. + const pkgJson = await fs.readJson(path.resolve(targetPath, 'package.json')); + + const options = { + ...extraConfig, + rootDir: path.resolve(targetPath, 'src'), + moduleNameMapper: { + '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), + }, + + // A bit more opinionated + testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`], + + transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], + ...getRoleConfig(pkgJson.backstage?.role, pkgJson), + }; + + options.setupFilesAfterEnv = options.setupFilesAfterEnv || []; + + if ( + extraOptions.rejectFrontendNetworkRequests && + FRONTEND_ROLES.includes(pkgJson.backstage?.role) + ) { + // By adding this first we ensure that it's possible to for example override + // fetch with a mock in a custom setup file + options.setupFilesAfterEnv.unshift( + require.resolve('./jestRejectNetworkRequests.js'), + ); + } + + if ( + options.testEnvironment === getJestEnvironment() && + getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+ + ) { + // FIXME https://github.com/jsdom/jsdom/issues/1724 + options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill')); + } + + // Use src/setupTests.* as the default location for configuring test env + for (const ext of SRC_EXTS) { + if (fs.existsSync(path.resolve(targetPath, `src/setupTests.${ext}`))) { + options.setupFilesAfterEnv.push(`/setupTests.${ext}`); + break; + } + } + + const config = Object.assign(options, pkgJson.jest); + + // The config id is a cache key that lets us share the jest cache across projects. + // If no explicit id was configured, generated one based on the configuration. + if (!config.id) { + const configHash = crypto + .createHash('sha256') + .update(version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config.transform).replaceAll(paths.targetRoot, '')) + .digest('hex'); + config.id = `backstage_cli_${configHash}`; + } + + return config; +} + +// This loads the root jest config, which in turn will either refer to a single +// configuration for the current package, or a collection of configurations for +// the target workspace packages +async function getRootConfig() { + const rootPkgJson = await fs.readJson( + paths.resolveTargetRoot('package.json'), + ); + + const baseCoverageConfig = { + coverageDirectory: paths.resolveTarget('coverage'), + coverageProvider: envOptions.oldTests ? 'v8' : 'babel', + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], + }; + + const { rejectFrontendNetworkRequests, ...rootOptions } = + rootPkgJson.jest ?? {}; + const extraRootOptions = { + rejectFrontendNetworkRequests, + }; + + const ws = rootPkgJson.workspaces; + const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages; + + // Check if we're running within a specific monorepo package. In that case just get the single project config. + if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { + return getProjectConfig( + paths.targetDir, + { + ...baseCoverageConfig, + ...rootOptions, + }, + extraRootOptions, + ); + } + + const globalRootConfig = { ...baseCoverageConfig }; + const globalProjectConfig = {}; + + for (const [key, value] of Object.entries(rootOptions)) { + if (projectConfigKeys.includes(key)) { + globalProjectConfig[key] = value; + } else { + globalRootConfig[key] = value; + } + } + + // If the target package is a workspace root, we find all packages in the + // workspace and load those in as separate jest projects instead. + const projectPaths = await Promise.all( + workspacePatterns.map(pattern => + glob(path.join(paths.targetRoot, pattern)), + ), + ).then(_ => _.flat()); + + let projects = await Promise.all( + projectPaths.flat().map(async projectPath => { + const packagePath = path.resolve(projectPath, 'package.json'); + if (!(await fs.pathExists(packagePath))) { + return undefined; + } + + // We check for the presence of "backstage-cli test" in the package test + // script to determine whether a given package should be tested + const packageData = await fs.readJson(packagePath); + const testScript = packageData.scripts && packageData.scripts.test; + const isSupportedTestScript = + testScript?.includes('backstage-cli test') || + testScript?.includes('backstage-cli package test'); + if (testScript && isSupportedTestScript) { + return await getProjectConfig( + projectPath, + { + ...globalProjectConfig, + displayName: packageData.name, + }, + extraRootOptions, + ); + } + + return undefined; + }), + ).then(cs => cs.filter(Boolean)); + + const cache = global.__backstageCli_jestSuccessCache; + if (cache) { + projects = await cache.filterConfigs(projects, globalRootConfig); + } + const watchProjectFilter = global.__backstageCli_watchProjectFilter; + if (watchProjectFilter) { + projects = await watchProjectFilter.filter(projects); + } + + return { + rootDir: paths.targetRoot, + projects, + testResultsProcessor: cache + ? require.resolve('./jestCacheResultProcessor.cjs') + : undefined, + ...globalRootConfig, + }; +} + +module.exports = getRootConfig(); diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs similarity index 72% rename from packages/cli/src/lib/versioning/index.ts rename to packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs index e0b9280ee6..4af401d8b5 100644 --- a/packages/cli/src/lib/versioning/index.ts +++ b/packages/cli-module-test-jest/config/jestCacheResultProcessor.cjs @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2024 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. @@ -14,6 +14,10 @@ * limitations under the License. */ -export { Lockfile } from './Lockfile'; -export { fetchPackageInfo, mapDependencies } from './packages'; -export type { YarnInfoInspectData } from './packages'; +module.exports = async results => { + const cache = global.__backstageCli_jestSuccessCache; + if (cache) { + await cache.reportResults(results); + } + return results; +}; diff --git a/packages/cli-module-test-jest/config/jestCachingModuleLoader.js b/packages/cli-module-test-jest/config/jestCachingModuleLoader.js new file mode 100644 index 0000000000..5652ff67e7 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestCachingModuleLoader.js @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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. + */ + +// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version +// eslint-disable-next-line @backstage/no-undeclared-imports +const { default: JestRuntime } = require('jest-runtime'); + +module.exports = class CachingJestRuntime extends JestRuntime { + constructor(config, ...restArgs) { + super(config, ...restArgs); + this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); + } + + // Unfortunately we need to use this unstable API to make sure that .js files + // are only loaded as modules where ESM is supported, i.e. Node.js packages. + unstable_shouldLoadAsEsm(path, ...restArgs) { + if (!this.allowLoadAsEsm) { + return false; + } + return super.unstable_shouldLoadAsEsm(path, ...restArgs); + } +}; diff --git a/packages/cli-module-test-jest/config/jestFileTransform.js b/packages/cli-module-test-jest/config/jestFileTransform.js new file mode 100644 index 0000000000..ad1c37a4f5 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestFileTransform.js @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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. + */ + +const path = require('node:path'); + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.icon\.svg$/)) { + return { + code: `const React = require('react'); + const SvgIcon = require('@material-ui/core/SvgIcon').default; + module.exports = { + __esModule: true, + default: props => React.createElement(SvgIcon, props, { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }) + };`, + }; + } + + return { code: `module.exports = ${assetFilename};` }; + }, +}; diff --git a/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js b/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js new file mode 100644 index 0000000000..f6f189a60c --- /dev/null +++ b/packages/cli-module-test-jest/config/jestRejectNetworkRequests.js @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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. + */ + +const http = require('node:http'); +const https = require('node:https'); + +const errorMessage = 'Network requests are not allowed in tests'; + +const origHttpAgent = http.globalAgent; +const origHttpsAgent = https.globalAgent; +const origFetch = global.fetch; +const origXMLHttpRequest = global.XMLHttpRequest; + +http.globalAgent = new http.Agent({ + lookup() { + throw new Error(errorMessage); + }, +}); + +https.globalAgent = new https.Agent({ + lookup() { + throw new Error(errorMessage); + }, +}); + +const BLOCKING_FETCH_SYMBOL = Symbol.for( + 'backstage.jestRejectNetworkRequests.blockingFetch', +); + +if (global.fetch) { + const blockingFetch = async (input, init) => { + // If global.fetch still has our marker, block the request + if (global.fetch[BLOCKING_FETCH_SYMBOL]) { + throw new Error(errorMessage); + } + // MSW (or something else) wrapped us - pass through + return origFetch(input, init); + }; + blockingFetch[BLOCKING_FETCH_SYMBOL] = true; + global.fetch = blockingFetch; +} + +if (global.XMLHttpRequest) { + global.XMLHttpRequest = class { + constructor() { + throw new Error(errorMessage); + } + }; +} + +// Reset overrides after each suite to make sure we don't pollute the test environment +afterAll(() => { + http.globalAgent = origHttpAgent; + https.globalAgent = origHttpsAgent; + global.fetch = origFetch; + global.XMLHttpRequest = origXMLHttpRequest; +}); diff --git a/packages/cli-module-test-jest/config/jestSucraseTransform.js b/packages/cli-module-test-jest/config/jestSucraseTransform.js new file mode 100644 index 0000000000..621b9ee297 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestSucraseTransform.js @@ -0,0 +1,87 @@ +/* + * Copyright 2021 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. + */ + +const { createHash } = require('node:crypto'); +const { transform } = require('sucrase'); +const sucrasePkg = require('sucrase/package.json'); + +const ESM_REGEX = /\b(?:import|export)\b/; + +function createTransformer(config) { + const process = (source, filePath) => { + let transforms; + + if (filePath.endsWith('.esm.js')) { + transforms = ['imports']; + } else if (filePath.endsWith('.js')) { + // This is a very rough filter to avoid transforming things that we quickly + // can be sure are definitely not ESM modules. + if (ESM_REGEX.test(source)) { + transforms = ['imports', 'jsx']; // JSX within .js is currently allowed + } + } else if (filePath.endsWith('.jsx')) { + transforms = ['jsx', 'imports']; + } else if (filePath.endsWith('.ts')) { + transforms = ['typescript', 'imports']; + } else if (filePath.endsWith('.tsx')) { + transforms = ['typescript', 'jsx', 'imports']; + } + + // Only apply the jest transform to the test files themselves + if (transforms && filePath.includes('.test.')) { + transforms.push('jest'); + } + + if (transforms) { + const { code, sourceMap: map } = transform(source, { + transforms, + filePath, + disableESTransforms: true, + sourceMapOptions: { + compiledFilename: filePath, + }, + }); + if (config.enableSourceMaps) { + const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64'); + const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`; + // Include both inline and object source maps, as inline source maps are + // needed for support of some editor integrations. + return { code: `${code}\n${suffix}`, map }; + } + // We only return the `map` result if source maps are enabled, as they + // have a negative impact on the coverage accuracy. + return { code }; + } + + return { code: source }; + }; + + const getCacheKey = sourceText => { + return createHash('sha256') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(sucrasePkg.version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config)) + .update(Buffer.alloc(1)) + .update('1') // increment whenever the transform logic in this file changes + .digest('hex'); + }; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/config/jestSwcTransform.js b/packages/cli-module-test-jest/config/jestSwcTransform.js new file mode 100644 index 0000000000..4183349554 --- /dev/null +++ b/packages/cli-module-test-jest/config/jestSwcTransform.js @@ -0,0 +1,44 @@ +/* + * Copyright 2022 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. + */ +const { createTransformer: createSwcTransformer } = require('@swc/jest'); + +const ESM_REGEX = /\b(?:import|export)\b/; + +function createTransformer(config) { + const swcTransformer = createSwcTransformer({ + inputSourceMap: false, + ...config, + }); + const process = (source, filePath, jestOptions) => { + // Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM + if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) { + return { code: source }; + } + + // Skip transformation of .mjs files, they should only be used if ESM support is available + if (jestOptions.supportsStaticESM && filePath.endsWith('.mjs')) { + return { code: source }; + } + + return swcTransformer.process(source, filePath, jestOptions); + }; + + const getCacheKey = swcTransformer.getCacheKey; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/config/jestYamlTransform.js b/packages/cli-module-test-jest/config/jestYamlTransform.js new file mode 100644 index 0000000000..7a05274add --- /dev/null +++ b/packages/cli-module-test-jest/config/jestYamlTransform.js @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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. + */ + +const yaml = require('yaml'); +const crypto = require('node:crypto'); + +function createTransformer(config) { + const process = source => { + const json = JSON.stringify(yaml.parse(source), null, 2); + return { code: `module.exports = ${json}`, map: null }; + }; + + const getCacheKey = sourceText => { + return crypto + .createHash('sha256') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config)) + .update(Buffer.alloc(1)) + .update('1') // increment whenever the transform logic in this file changes + .digest('hex'); + }; + + return { process, getCacheKey }; +} + +module.exports = { createTransformer }; diff --git a/packages/cli-module-test-jest/package.json b/packages/cli-module-test-jest/package.json new file mode 100644 index 0000000000..6d7811f484 --- /dev/null +++ b/packages/cli-module-test-jest/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/cli-module-test-jest", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-test-jest" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin", + "config" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@swc/core": "^1.15.6", + "@swc/jest": "^0.2.39", + "cleye": "^2.3.0", + "cross-fetch": "^4.0.0", + "fs-extra": "^11.2.0", + "glob": "^7.1.7", + "jest-css-modules": "^2.1.0", + "sucrase": "^3.20.2", + "yargs": "^16.2.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "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 + }, + "jest-environment-jsdom": { + "optional": true + }, + "jsdom": { + "optional": true + } + }, + "bin": "bin/backstage-cli-module-test-jest" +} diff --git a/packages/cli-module-test-jest/report.api.md b/packages/cli-module-test-jest/report.api.md new file mode 100644 index 0000000000..816df9dcf1 --- /dev/null +++ b/packages/cli-module-test-jest/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-test-jest" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli-module-test-jest/src/commands/package/test.ts similarity index 83% rename from packages/cli/src/modules/test/commands/package/test.ts rename to packages/cli-module-test-jest/src/commands/package/test.ts index 992697d19c..b260601f8e 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli-module-test-jest/src/commands/package/test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { Command, OptionValues } from 'commander'; -import { paths } from '../../../../lib/paths'; -import { runCheck } from '@backstage/cli-common'; +import { findOwnPaths, runCheck } from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -27,18 +26,11 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { return false; } -export default async (_opts: OptionValues, cmd: Command) => { - // all args are forwarded to jest - let parent = cmd; - while (parent.parent) { - parent = parent.parent; - } - const allArgs = parent.args as string[]; - const args = allArgs.slice(allArgs.indexOf('test') + 1); - +export default async ({ args }: CliCommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { - args.push('--config', paths.resolveOwn('config/jest.js')); + /* eslint-disable-next-line no-restricted-syntax */ + args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); } if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { @@ -86,11 +78,6 @@ export default async (_opts: OptionValues, cmd: Command) => { }--no-node-snapshot`; } - // This ensures that the process doesn't exit too early before stdout is flushed - if (args.includes('--help')) { - (process.stdout as any)._handle.setBlocking(true); - } - // Because of the ongoing migration to v30 of jest, jest is no longer hard-depended to allow // opt-in migration. Users instead need to add jest as a devDependency themselves and specify // the version they want. This prints a helpful error message if jest is not found, i.e. they @@ -109,5 +96,6 @@ export default async (_opts: OptionValues, cmd: Command) => { process.exit(1); } + // eslint-disable-next-line @backstage/no-undeclared-imports await require('jest').run(args); }; diff --git a/packages/cli-module-test-jest/src/commands/repo/test.test.ts b/packages/cli-module-test-jest/src/commands/repo/test.test.ts new file mode 100644 index 0000000000..fe0e733f8c --- /dev/null +++ b/packages/cli-module-test-jest/src/commands/repo/test.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2020 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 { cli } from 'cleye'; +import { createFlagFinder } from './test'; + +describe('createFlagFinder', () => { + it('finds flags', () => { + const find = createFlagFinder([ + '--foo', + '--no-bar', + '-b', + '-c', + '--baz=1', + '--qux', + '2', + '-de', + ]); + + expect( + find('--foo', '--bar', '-b', '-c', '--baz', '--qux', '-d', '-e'), + ).toBe(true); + expect(find('--foo')).toBe(true); + expect(find('--bar')).toBe(true); + expect(find('--no-bar')).toBe(false); + expect(find('-a')).toBe(false); + expect(find('-b')).toBe(true); + expect(find('-c')).toBe(true); + expect(find('-d')).toBe(true); + expect(find('-e')).toBe(true); + expect(find('--baz')).toBe(true); + expect(find('--qux')).toBe(true); + expect(find('--qux')).toBe(true); + }); +}); + +describe('repo test arg forwarding', () => { + // Mirrors the cleye configuration used in the repo test command handler + function parseRepoTestArgs(args: string[]) { + return cli( + { + help: false, + flags: { + since: { type: String }, + successCache: { type: Boolean }, + successCacheDir: { type: String }, + jestHelp: { type: Boolean }, + }, + ignoreArgv: type => type === 'unknown-flag' || type === 'argument', + }, + undefined, + args, + ); + } + + it('strips Backstage flags from args while preserving Jest flags and arguments', () => { + const args = [ + '--since', + 'main', + '--success-cache', + '--coverage', + '--watch', + 'path/to/test', + ]; + + const { flags } = parseRepoTestArgs(args); + + expect(flags.since).toBe('main'); + expect(flags.successCache).toBe(true); + expect(args).toEqual(['--coverage', '--watch', 'path/to/test']); + }); + + it('supports legacy camelCase flag names', () => { + const args = ['--successCache', '--successCacheDir', '/tmp/cache']; + + const { flags } = parseRepoTestArgs(args); + + expect(flags.successCache).toBe(true); + expect(flags.successCacheDir).toBe('/tmp/cache'); + expect(args).toEqual([]); + }); + + it('leaves args untouched when no Backstage flags are present', () => { + const args = ['--coverage', '--verbose', '--bail']; + + parseRepoTestArgs(args); + + expect(args).toEqual(['--coverage', '--verbose', '--bail']); + }); +}); diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli-module-test-jest/src/commands/repo/test.ts similarity index 86% rename from packages/cli/src/modules/test/commands/repo/test.ts rename to packages/cli-module-test-jest/src/commands/repo/test.ts index f4d488ee0e..806d88965b 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli-module-test-jest/src/commands/repo/test.ts @@ -16,17 +16,22 @@ import os from 'node:os'; import crypto from 'node:crypto'; +import { cli } from 'cleye'; import yargs from 'yargs'; // 'jest-cli' is included with jest and should be kept in sync with the installed jest version // eslint-disable-next-line @backstage/no-undeclared-imports import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli'; import { relative as relativePath } from 'node:path'; -import { Command, OptionValues } from 'commander'; -import { Lockfile, PackageGraph } from '@backstage/cli-node'; -import { paths } from '../../../../lib/paths'; -import { runCheck, runOutput } from '@backstage/cli-common'; -import { isChildPath } from '@backstage/cli-common'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; +import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; + +import { + findOwnPaths, + runCheck, + runOutput, + targetPaths, + isChildPath, +} from '@backstage/cli-common'; +import type { CliCommandContext } from '@backstage/cli-node'; type JestProject = { displayName: string; @@ -63,7 +68,7 @@ interface TestGlobal extends Global { async function readPackageTreeHashes(graph: PackageGraph) { const pkgs = Array.from(graph.values()).map(pkg => ({ ...pkg, - path: relativePath(paths.targetRoot, pkg.dir), + path: relativePath(targetPaths.rootDir, pkg.dir), })); const output = await runOutput([ 'git', @@ -126,43 +131,58 @@ export function createFlagFinder(args: string[]) { }; } -function removeOptionArg(args: string[], option: string, size: number = 2) { - let changed = false; - do { - changed = false; - - const index = args.indexOf(option); - if (index >= 0) { - changed = true; - args.splice(index, size); - } - const indexEq = args.findIndex(arg => arg.startsWith(`${option}=`)); - if (indexEq >= 0) { - changed = true; - args.splice(indexEq, 1); - } - } while (changed); -} - -export async function command(opts: OptionValues, cmd: Command): Promise { +export default async ({ args, info }: CliCommandContext) => { const testGlobal = global as TestGlobal; - // all args are forwarded to jest - let parent = cmd; - while (parent.parent) { - parent = parent.parent; + for (const flag of ['successCache', 'successCacheDir', 'jestHelp']) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } } - const allArgs = parent.args as string[]; - const args = allArgs.slice(allArgs.indexOf('test') + 1); + + // Parse Backstage-specific flags; unknown flags and arguments are left in + // args so they can be forwarded to Jest. + const { flags: opts } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + since: { + type: String, + description: + 'Only include test packages changed since the specified ref', + }, + successCache: { + type: Boolean, + description: 'Cache and skip tests for unchanged packages', + }, + successCacheDir: { + type: String, + description: 'Directory for the success cache', + }, + jestHelp: { + type: Boolean, + description: "Show Jest's own help output", + }, + }, + ignoreArgv: type => type === 'unknown-flag' || type === 'argument', + }, + undefined, + args, + ); const hasFlags = createFlagFinder(args); + const sinceRef = opts.since || undefined; // Parse the args to ensure that no file filters are provided, in which case we refuse to run const { _: parsedArgs } = await yargs(args).options(jestYargsOptions).argv; // Only include our config if caller isn't passing their own config if (!hasFlags('-c', '--config')) { - args.push('--config', paths.resolveOwn('config/jest.js')); + /* eslint-disable-next-line no-restricted-syntax */ + args.push('--config', findOwnPaths(__dirname).resolve('config/jest.js')); } if (!hasFlags('--passWithNoTests')) { @@ -172,7 +192,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { // Run in watch mode unless in CI, coverage mode, or running all tests let isSingleWatchMode = args.includes('--watch'); if ( - !opts.since && + !sinceRef && !process.env.CI && !hasFlags('--coverage', '--watch', '--watchAll') ) { @@ -241,10 +261,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise { args.push('--maxWorkers=2'); } - if (opts.since) { - removeOptionArg(args, '--since'); - } - let packageGraph: PackageGraph | undefined; async function getPackageGraph() { if (packageGraph) { @@ -256,10 +272,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } let selectedProjects: string[] | undefined = undefined; - if (opts.since && !hasFlags('--selectProjects')) { + if (sinceRef && !hasFlags('--selectProjects')) { const graph = await getPackageGraph(); const changedPackages = await graph.listChangedPackages({ - ref: opts.since, + ref: sinceRef, analyzeLockfile: true, }); @@ -299,19 +315,13 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }--no-node-snapshot`; } - // This ensures that the process doesn't exit too early before stdout is flushed - if (args.includes('--jest-help')) { - removeOptionArg(args, '--jest-help'); + if (opts.jestHelp) { args.push('--help'); - (process.stdout as any)._handle.setBlocking(true); } // This code path is enabled by the --successCache flag, which is specific to // the `repo test` command in the Backstage CLI. if (opts.successCache) { - removeOptionArg(args, '--successCache', 1); - removeOptionArg(args, '--successCacheDir'); - // Refuse to run if file filters are provided if (parsedArgs.length > 0) { throw new Error( @@ -327,7 +337,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); } - const cache = new SuccessCache('test', opts.successCacheDir); + const cache = SuccessCache.create({ + name: 'test', + basePath: opts.successCacheDir, + }); const graph = await getPackageGraph(); // Shared state for the bridge @@ -341,7 +354,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { async filterConfigs(projectConfigs, globalRootConfig) { const cacheEntries = await cache.read(); const lockfile = await Lockfile.load( - paths.resolveTargetRoot('yarn.lock'), + targetPaths.resolveRoot('yarn.lock'), ); const getPackageTreeHash = await readPackageTreeHashes(graph); @@ -433,4 +446,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } await runJest(args); -} +}; diff --git a/packages/cli-module-test-jest/src/index.ts b/packages/cli-module-test-jest/src/index.ts new file mode 100644 index 0000000000..b2060fa89e --- /dev/null +++ b/packages/cli-module-test-jest/src/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['repo', 'test'], + description: + 'Run tests, forwarding args to Jest, defaulting to watch mode', + execute: { loader: () => import('./commands/repo/test') }, + }); + + reg.addCommand({ + path: ['package', 'test'], + description: + 'Run tests, forwarding args to Jest, defaulting to watch mode', + execute: { loader: () => import('./commands/package/test') }, + }); + }, +}); diff --git a/packages/cli-module-translations/.eslintrc.js b/packages/cli-module-translations/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli-module-translations/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-module-translations/README.md b/packages/cli-module-translations/README.md new file mode 100644 index 0000000000..1f415ca975 --- /dev/null +++ b/packages/cli-module-translations/README.md @@ -0,0 +1,15 @@ +# @backstage/cli-module-translations + +CLI module that provides translation management commands for the Backstage CLI. + +## Commands + +| Command | Description | +| :-------------------- | :------------------------------------------------------------------------------------ | +| `translations export` | Export translation messages from an app and all of its frontend plugins to JSON files | +| `translations import` | Generate translation resource wiring from translated JSON files | + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/cli-module-translations/bin/backstage-cli-module-translations b/packages/cli-module-translations/bin/backstage-cli-module-translations new file mode 100755 index 0000000000..68bdc76a4a --- /dev/null +++ b/packages/cli-module-translations/bin/backstage-cli-module-translations @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/* + * Copyright 2024 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. + */ + +const path = require('node:path'); + +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('node:fs').existsSync( + path.resolve(__dirname, '../src'), +); + +if (isLocal) { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} + +const { runCliModule } = require('@backstage/cli-node'); +const cliModule = require(isLocal ? '../src/index' : '..').default; +const pkg = require('../package.json'); +runCliModule({ module: cliModule, name: pkg.name, version: pkg.version }); diff --git a/packages/cli-module-translations/catalog-info.yaml b/packages/cli-module-translations/catalog-info.yaml new file mode 100644 index 0000000000..a2f21fc649 --- /dev/null +++ b/packages/cli-module-translations/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-module-translations + title: '@backstage/cli-module-translations' + description: CLI module for Backstage CLI +spec: + lifecycle: experimental + type: backstage-cli-module + owner: tooling-maintainers diff --git a/packages/cli-module-translations/cli-report.md b/packages/cli-module-translations/cli-report.md new file mode 100644 index 0000000000..bf2ff28001 --- /dev/null +++ b/packages/cli-module-translations/cli-report.md @@ -0,0 +1,53 @@ +## CLI Report file for "@backstage/cli-module-translations" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-cli-module-translations` + +``` +Usage: @backstage/cli-module-translations [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + help [command] + translations [command] +``` + +### `backstage-cli-module-translations translations` + +``` +Usage: @backstage/cli-module-translations translations [options] [command] [command] + +Options: + -h, --help + +Commands: + export + help [command] + import +``` + +### `backstage-cli-module-translations translations export` + +``` +Usage: @backstage/cli-module-translations translations export + +Options: + --output + --pattern + -h, --help +``` + +### `backstage-cli-module-translations translations import` + +``` +Usage: @backstage/cli-module-translations translations import + +Options: + --input + --output + -h, --help +``` diff --git a/packages/cli-module-translations/package.json b/packages/cli-module-translations/package.json new file mode 100644 index 0000000000..86ef2b7ca4 --- /dev/null +++ b/packages/cli-module-translations/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/cli-module-translations", + "version": "0.0.0", + "description": "CLI module for Backstage CLI", + "backstage": { + "role": "cli-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-module-translations" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "bin" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "cleye": "^2.3.0", + "fs-extra": "^11.2.0", + "ts-morph": "^24.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^11.0.0" + }, + "bin": "bin/backstage-cli-module-translations" +} diff --git a/packages/cli-module-translations/report.api.md b/packages/cli-module-translations/report.api.md new file mode 100644 index 0000000000..5e5864bea5 --- /dev/null +++ b/packages/cli-module-translations/report.api.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/cli-module-translations" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CliModule } from '@backstage/cli-node'; + +// @public (undocumented) +const _default: CliModule; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-module-translations/src/commands/export.ts b/packages/cli-module-translations/src/commands/export.ts new file mode 100644 index 0000000000..a1d2eb2dae --- /dev/null +++ b/packages/cli-module-translations/src/commands/export.ts @@ -0,0 +1,163 @@ +/* + * Copyright 2026 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 { cli } from 'cleye'; +import { targetPaths } from '@backstage/cli-common'; +import fs from 'fs-extra'; +import { dirname, resolve as resolvePath } from 'node:path'; +import { + discoverFrontendPackages, + readTargetPackage, +} from '../lib/discoverPackages'; +import { + createTranslationProject, + extractTranslationRefsFromSourceFile, + TranslationRefInfo, +} from '../lib/extractTranslations'; +import { + DEFAULT_LANGUAGE, + DEFAULT_MESSAGE_PATTERN, + formatMessagePath, + validatePattern, +} from '../lib/messageFilePath'; +import type { CliCommandContext } from '@backstage/cli-node'; + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { output, pattern }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + output: { + type: String, + default: 'translations', + description: 'Output directory for exported messages and manifest', + }, + pattern: { + type: String, + default: DEFAULT_MESSAGE_PATTERN, + description: + 'File path pattern for message files, with {id} and {lang} placeholders', + }, + }, + }, + undefined, + args, + ); + + const options = { output, pattern }; + + validatePattern(options.pattern); + + const targetPackageJson = await readTargetPackage( + targetPaths.dir, + targetPaths.rootDir, + ); + + const outputDir = resolvePath(targetPaths.dir, options.output); + const manifestPath = resolvePath(outputDir, 'manifest.json'); + + const tsconfigPath = targetPaths.resolveRoot('tsconfig.json'); + if (!(await fs.pathExists(tsconfigPath))) { + throw new Error( + `No tsconfig.json found at ${tsconfigPath}. ` + + 'The translations export command requires a tsconfig.json in the repo root.', + ); + } + + console.log( + `Discovering frontend dependencies of ${targetPackageJson.name}...`, + ); + const packages = await discoverFrontendPackages( + targetPackageJson, + targetPaths.dir, + ); + console.log(`Found ${packages.length} frontend packages to scan`); + + console.log('Creating TypeScript project...'); + const project = createTranslationProject(tsconfigPath); + + const allRefs: TranslationRefInfo[] = []; + + for (const pkg of packages) { + for (const [exportPath, filePath] of pkg.entryPoints) { + try { + const sourceFile = project.addSourceFileAtPath(filePath); + const refs = extractTranslationRefsFromSourceFile( + sourceFile, + pkg.name, + exportPath, + ); + allRefs.push(...refs); + } catch (error) { + console.warn( + ` Warning: failed to process ${pkg.name} (${exportPath}): ${error}`, + ); + } + } + } + + if (allRefs.length === 0) { + console.log('No translation refs found.'); + return; + } + + console.log(`Found ${allRefs.length} translation ref(s):`); + for (const ref of allRefs) { + const messageCount = Object.keys(ref.messages).length; + console.log(` ${ref.id} (${ref.packageName}, ${messageCount} messages)`); + } + + // Write message files using the configured pattern + for (const ref of allRefs) { + const relPath = formatMessagePath( + options.pattern, + ref.id, + DEFAULT_LANGUAGE, + ); + const filePath = resolvePath(outputDir, relPath); + await fs.ensureDir(dirname(filePath)); + await fs.writeJson(filePath, ref.messages, { spaces: 2 }); + } + + // Write manifest + const manifest: Record = {}; + for (const ref of allRefs) { + manifest[ref.id] = { + package: ref.packageName, + exportPath: ref.exportPath, + exportName: ref.exportName, + }; + } + await fs.writeJson( + manifestPath, + { pattern: options.pattern, refs: manifest }, + { spaces: 2 }, + ); + + const examplePath = formatMessagePath( + options.pattern, + '', + DEFAULT_LANGUAGE, + ); + console.log( + `\nExported ${allRefs.length} translation ref(s) to ${options.output}/`, + ); + console.log(` Messages: ${options.output}/${examplePath}`); + console.log(` Manifest: ${options.output}/manifest.json`); +}; diff --git a/packages/cli-module-translations/src/commands/import.ts b/packages/cli-module-translations/src/commands/import.ts new file mode 100644 index 0000000000..35a071c4fb --- /dev/null +++ b/packages/cli-module-translations/src/commands/import.ts @@ -0,0 +1,236 @@ +/* + * Copyright 2026 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 { cli } from 'cleye'; +import { targetPaths } from '@backstage/cli-common'; +import fs from 'fs-extra'; +import { + resolve as resolvePath, + relative as relativePath, + sep, +} from 'node:path'; +import { readTargetPackage } from '../lib/discoverPackages'; +import { + DEFAULT_LANGUAGE, + createMessagePathParser, + formatMessagePath, +} from '../lib/messageFilePath'; +import type { CliCommandContext } from '@backstage/cli-node'; + +interface ManifestRefEntry { + package: string; + exportPath: string; + exportName: string; +} + +interface Manifest { + pattern?: string; + refs: Record; +} + +export default async ({ args, info }: CliCommandContext) => { + const { + flags: { input, output }, + } = cli( + { + help: info, + booleanFlagNegation: true, + flags: { + input: { + type: String, + default: 'translations', + description: + 'Input directory containing the manifest and translated message files', + }, + output: { + type: String, + default: 'src/translations/resources.ts', + description: 'Output path for the generated wiring module', + }, + }, + }, + undefined, + args, + ); + + const options = { input, output }; + await readTargetPackage(targetPaths.dir, targetPaths.rootDir); + + const inputDir = resolvePath(targetPaths.dir, options.input); + const manifestPath = resolvePath(inputDir, 'manifest.json'); + const outputPath = resolvePath(targetPaths.dir, options.output); + + if (!(await fs.pathExists(manifestPath))) { + throw new Error( + `No manifest.json found at ${manifestPath}. ` + + 'Run "backstage-cli translations export" first.', + ); + } + + const manifest: Manifest = await fs.readJson(manifestPath); + + if (!manifest.pattern) { + throw new Error( + 'No pattern found in manifest.json. Re-run "backstage-cli translations export" to regenerate it.', + ); + } + const pattern = manifest.pattern; + + const parsePath = createMessagePathParser(pattern); + + // Discover all JSON files under the translations directory + const allFiles = (await collectJsonFiles(inputDir)).filter( + f => f !== 'manifest.json', + ); + + // Parse each file to extract id + lang, filtering out default language files + const translationsByRef = new Map< + string, + Array<{ lang: string; relPath: string }> + >(); + let skipped = 0; + + for (const relPath of allFiles) { + const parsed = parsePath(relPath); + if (!parsed) { + skipped++; + continue; + } + + if (parsed.lang === DEFAULT_LANGUAGE) { + continue; + } + + if (!manifest.refs[parsed.id]) { + console.warn( + ` Warning: skipping ${relPath} - ref '${parsed.id}' not found in manifest`, + ); + continue; + } + + const existing = translationsByRef.get(parsed.id) ?? []; + existing.push({ lang: parsed.lang, relPath }); + translationsByRef.set(parsed.id, existing); + } + + if (skipped > 0) { + console.warn( + ` Warning: ${skipped} file(s) did not match the pattern '${pattern}'`, + ); + } + + if (translationsByRef.size === 0) { + console.log('No translated message files found.'); + const example = formatMessagePath(pattern, '', 'sv'); + console.log( + `Add translated files as ${example} in the translations directory.`, + ); + return; + } + + // Generate the wiring module + const importLines: string[] = []; + const resourceLines: string[] = []; + + importLines.push( + "import { createTranslationResource } from '@backstage/frontend-plugin-api';", + ); + + for (const [refId, entries] of [...translationsByRef.entries()].sort( + ([a], [b]) => a.localeCompare(b), + )) { + const refEntry = manifest.refs[refId]; + const importPath = + refEntry.exportPath === '.' + ? refEntry.package + : `${refEntry.package}/${refEntry.exportPath.replace(/^\.\//, '')}`; + + importLines.push(`import { ${refEntry.exportName} } from '${importPath}';`); + + const translationEntries = entries + .sort((a, b) => a.lang.localeCompare(b.lang)) + .map(({ lang, relPath }) => { + const jsonRelPath = relativePath( + resolvePath(outputPath, '..'), + resolvePath(inputDir, relPath), + ) + .split(sep) + .join('/'); + return ` ${JSON.stringify(lang)}: () => import('./${jsonRelPath}'),`; + }) + .join('\n'); + + resourceLines.push( + [ + ` createTranslationResource({`, + ` ref: ${refEntry.exportName},`, + ` translations: {`, + translationEntries, + ` },`, + ` }),`, + ].join('\n'), + ); + } + + const fileContent = [ + '// This file is auto-generated by backstage-cli translations import', + '// Do not edit manually.', + '', + ...importLines, + '', + 'export default [', + ...resourceLines, + '];', + '', + ].join('\n'); + + await fs.ensureDir(resolvePath(outputPath, '..')); + await fs.writeFile(outputPath, fileContent, 'utf8'); + + const totalFiles = [...translationsByRef.values()].reduce( + (sum, e) => sum + e.length, + 0, + ); + console.log(`Generated translation resources at ${options.output}`); + console.log( + ` ${translationsByRef.size} ref(s), ${totalFiles} translation file(s)`, + ); + console.log( + '\nImport this file in your app and pass the resources to your translation API setup.', + ); +}; + +/** + * Recursively collects all .json files under a directory, returning paths + * relative to that directory using forward slashes. + */ +async function collectJsonFiles(dir: string, prefix = ''): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const results: string[] = []; + + for (const entry of entries) { + const relPath = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + results.push( + ...(await collectJsonFiles(resolvePath(dir, entry.name), relPath)), + ); + } else if (entry.isFile() && entry.name.endsWith('.json')) { + results.push(relPath); + } + } + + return results; +} diff --git a/packages/cli-module-translations/src/index.ts b/packages/cli-module-translations/src/index.ts new file mode 100644 index 0000000000..e075dc61ae --- /dev/null +++ b/packages/cli-module-translations/src/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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 { createCliModule } from '@backstage/cli-node'; +import packageJson from '../package.json'; + +export default createCliModule({ + packageJson, + init: async reg => { + reg.addCommand({ + path: ['translations', 'export'], + description: + 'Export translation messages from an app and all of its frontend plugins to JSON files', + execute: { loader: () => import('./commands/export') }, + }); + + reg.addCommand({ + path: ['translations', 'import'], + description: + 'Generate translation resource wiring from translated JSON files', + execute: { loader: () => import('./commands/import') }, + }); + }, +}); diff --git a/packages/cli-module-translations/src/lib/discoverPackages.ts b/packages/cli-module-translations/src/lib/discoverPackages.ts new file mode 100644 index 0000000000..c7a7602a7e --- /dev/null +++ b/packages/cli-module-translations/src/lib/discoverPackages.ts @@ -0,0 +1,207 @@ +/* + * Copyright 2026 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 { + BackstagePackageJson, + PackageGraph, + PackageRoles, +} from '@backstage/cli-node'; +import { dirname, resolve as resolvePath } from 'node:path'; +import fs from 'fs-extra'; + +/** A discovered package with its entry points resolved to file paths. */ +export interface DiscoveredPackage { + /** The package name, e.g. '@backstage/plugin-org' */ + name: string; + /** The directory of the package */ + dir: string; + /** Map of export subpath (e.g. '.', './alpha') to the resolved file path */ + entryPoints: Map; +} + +/** + * Reads the package.json from the given directory and validates that it + * is a workspace package (not the repo root). + */ +export async function readTargetPackage( + packageDir: string, + repoRoot: string, +): Promise { + const packageJsonPath = resolvePath(packageDir, 'package.json'); + + if (!(await fs.pathExists(packageJsonPath))) { + throw new Error( + 'No package.json found in the current directory. ' + + 'The translations commands must be run from within a package directory.', + ); + } + + if (resolvePath(packageDir) === resolvePath(repoRoot)) { + throw new Error( + 'The translations commands must be run from within a package directory, ' + + 'not from the repository root. For example: cd packages/app && backstage-cli translations export', + ); + } + + return fs.readJson(packageJsonPath); +} + +/** + * Discovers frontend packages that are transitive dependencies of the given + * target package and resolves their entry point file paths. Walks both + * workspace packages (source) and npm-installed packages (declaration files). + */ +export async function discoverFrontendPackages( + targetPackageJson: BackstagePackageJson, + targetDir: string, +): Promise { + // Build a lookup of workspace packages for preferring source over dist + let workspaceByName: Map< + string, + { packageJson: BackstagePackageJson; dir: string } + >; + try { + const workspacePackages = await PackageGraph.listTargetPackages(); + workspaceByName = new Map( + workspacePackages.map(p => [p.packageJson.name, p]), + ); + } catch { + workspaceByName = new Map(); + } + + const visited = new Set(); + const result: DiscoveredPackage[] = []; + + async function visit( + packageJson: BackstagePackageJson, + pkgDir: string, + includeDevDeps: boolean, + ) { + const deps: Record = { + ...packageJson.dependencies, + ...(includeDevDeps ? packageJson.devDependencies ?? {} : {}), + }; + + for (const depName of Object.keys(deps)) { + if (visited.has(depName)) { + continue; + } + visited.add(depName); + + let depPkgJson: BackstagePackageJson; + let depDir: string; + let isWorkspace: boolean; + + // Prefer workspace package (has source files) over npm-installed + const workspacePkg = workspaceByName.get(depName); + if (workspacePkg) { + depPkgJson = workspacePkg.packageJson; + depDir = workspacePkg.dir; + isWorkspace = true; + } else { + try { + const pkgJsonPath = require.resolve(`${depName}/package.json`, { + paths: [pkgDir], + }); + depPkgJson = await fs.readJson(pkgJsonPath); + depDir = dirname(pkgJsonPath); + isWorkspace = false; + } catch { + continue; + } + } + + // Only recurse into Backstage ecosystem packages + if (!depPkgJson.backstage) { + continue; + } + + const role = depPkgJson.backstage?.role; + if (role && isFrontendRole(role)) { + const entryPoints = resolveEntryPoints(depPkgJson, depDir, isWorkspace); + if (entryPoints.size > 0) { + result.push({ name: depName, dir: depDir, entryPoints }); + } + } + + // Walk this package's production dependencies for transitive refs + await visit(depPkgJson, depDir, false); + } + } + + // Start from the target, including its devDependencies + await visit(targetPackageJson, targetDir, true); + + return result; +} + +/** + * Resolves the entry points of a package to absolute file paths. + * For workspace packages, prefers source entry points (import/default). + * For npm packages, prefers type declaration entry points (.d.ts). + */ +function resolveEntryPoints( + packageJson: BackstagePackageJson, + packageDir: string, + isWorkspace: boolean, +): Map { + const entryPoints = new Map(); + + const exports = (packageJson as any).exports as + | Record> + | undefined; + + if (exports) { + for (const [subpath, target] of Object.entries(exports)) { + if (subpath === './package.json') { + continue; + } + + let filePath: string | undefined; + if (typeof target === 'string') { + filePath = target; + } else if (isWorkspace) { + // Workspace: exports point to source .ts files + filePath = target?.import ?? target?.types ?? target?.default; + } else { + // npm: prefer .d.ts for type-based extraction + filePath = target?.types ?? target?.import ?? target?.default; + } + + if (typeof filePath === 'string') { + entryPoints.set(subpath, resolvePath(packageDir, filePath)); + } + } + } else { + // Fallback: prefer types for npm, source for workspace + const main = isWorkspace + ? packageJson.main ?? packageJson.types + : packageJson.types ?? packageJson.main; + if (main) { + entryPoints.set('.', resolvePath(packageDir, main)); + } + } + + return entryPoints; +} + +function isFrontendRole(role: string): boolean { + try { + return PackageRoles.getRoleInfo(role).platform === 'web'; + } catch { + return false; + } +} diff --git a/packages/cli-module-translations/src/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts new file mode 100644 index 0000000000..13a94f3a0a --- /dev/null +++ b/packages/cli-module-translations/src/lib/extractTranslations.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2026 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 { resolve as resolvePath } from 'node:path'; +import { + createTranslationProject, + extractTranslationRefsFromSourceFile, +} from './extractTranslations'; + +describe('extractTranslations', () => { + it('extracts translation refs from the org plugin', () => { + const project = createTranslationProject( + resolvePath(__dirname, '../../../../tsconfig.json'), + ); + + const sourceFile = project.addSourceFileAtPath( + resolvePath(__dirname, '../../../..', 'plugins/org/src/alpha.tsx'), + ); + + const refs = extractTranslationRefsFromSourceFile( + sourceFile, + '@backstage/plugin-org', + './alpha', + ); + + expect(refs).toHaveLength(1); + expect(refs[0]).toMatchObject({ + id: 'org', + packageName: '@backstage/plugin-org', + exportPath: './alpha', + exportName: 'orgTranslationRef', + }); + + expect(refs[0].messages).toBeDefined(); + expect(Object.keys(refs[0].messages)).not.toHaveLength(0); + + // Verify some well-known keys exist without pinning exact wording + expect(refs[0].messages).toHaveProperty(['groupProfileCard.groupNotFound']); + expect(refs[0].messages).toHaveProperty(['membersListCard.title']); + + // Verify interpolation placeholders are preserved + expect(refs[0].messages['membersListCard.title']).toContain( + '{{groupName}}', + ); + }); + + it('ignores non-TranslationRef exports', () => { + const project = createTranslationProject( + resolvePath(__dirname, '../../../../tsconfig.json'), + ); + + // The main entry of org plugin exports components but no translation ref + const sourceFile = project.addSourceFileAtPath( + resolvePath(__dirname, '../../../..', 'plugins/org/src/index.ts'), + ); + + const refs = extractTranslationRefsFromSourceFile( + sourceFile, + '@backstage/plugin-org', + '.', + ); + + expect(refs).toHaveLength(0); + }); + + it('extracts from the test fixtures translation ref', () => { + const project = createTranslationProject( + resolvePath(__dirname, '../../../../tsconfig.json'), + ); + + const sourceFile = project.addSourceFileAtPath( + resolvePath( + __dirname, + '../../../..', + 'packages/frontend-plugin-api/src/translation/__fixtures__/refs.ts', + ), + ); + + const refs = extractTranslationRefsFromSourceFile( + sourceFile, + '@backstage/frontend-plugin-api', + '.', + ); + + expect(refs).toHaveLength(2); + + const counting = refs.find(r => r.id === 'counting'); + expect(counting).toMatchObject({ + messages: { + one: 'one', + two: 'two', + three: 'three', + }, + }); + + const fruits = refs.find(r => r.id === 'fruits'); + expect(fruits).toMatchObject({ + messages: { + apple: 'apple', + orange: 'orange', + }, + }); + }); +}); diff --git a/packages/cli-module-translations/src/lib/extractTranslations.ts b/packages/cli-module-translations/src/lib/extractTranslations.ts new file mode 100644 index 0000000000..6278a08598 --- /dev/null +++ b/packages/cli-module-translations/src/lib/extractTranslations.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2026 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 { Node, Project, SourceFile, Type, ts } from 'ts-morph'; + +/** Information about a discovered translation ref. */ +export interface TranslationRefInfo { + /** The ref ID, e.g. 'org' */ + id: string; + /** The package name, e.g. '@backstage/plugin-org' */ + packageName: string; + /** The subpath export where this ref is accessible, e.g. './alpha' or '.' */ + exportPath: string; + /** The exported symbol name, e.g. 'orgTranslationRef' */ + exportName: string; + /** Flattened message map: key -> default message string */ + messages: Record; +} + +/** + * Given a ts-morph SourceFile, finds all exported TranslationRef symbols + * and extracts their id and messages from the type system. + */ +export function extractTranslationRefsFromSourceFile( + sourceFile: SourceFile, + packageName: string, + exportPath: string, +): TranslationRefInfo[] { + const results: TranslationRefInfo[] = []; + + for (const exportSymbol of sourceFile.getExportSymbols()) { + const declarations = exportSymbol.getDeclarations(); + if (declarations.length === 0) { + continue; + } + + const declaration = declarations[0]; + const exportType = declaration.getType(); + + const refInfo = extractTranslationRefFromType(exportType, declaration); + if (!refInfo) { + continue; + } + + results.push({ + ...refInfo, + packageName, + exportPath, + exportName: exportSymbol.getName(), + }); + } + + return results; +} + +/** + * Checks whether a type is a TranslationRef by inspecting the $$type + * property on the target type, then extracts the id and messages from + * the type arguments of the generic instantiation. + */ +function extractTranslationRefFromType( + type: Type, + declaration: Node, +): Pick | undefined { + // Check the $$type property on the uninstantiated (target) type + const resolvedType = type.getTargetType() ?? type; + const $$typeProperty = resolvedType + .getProperties() + .find(p => p.getName() === '$$type'); + if (!$$typeProperty) { + return undefined; + } + const $$typeDecl = $$typeProperty.getValueDeclaration(); + if (!$$typeDecl) { + return undefined; + } + if (!$$typeDecl.getText().includes("'@backstage/TranslationRef'")) { + return undefined; + } + + // The type is TranslationRef - extract the type arguments + const typeArgs = type.getTypeArguments(); + if (typeArgs.length < 2) { + return undefined; + } + + const [idType, messagesType] = typeArgs; + + if (!idType.isStringLiteral()) { + return undefined; + } + const id = idType.getLiteralValueOrThrow() as string; + + // Extract messages from the TMessages type argument + const messages: Record = {}; + for (const messageProp of messagesType.getProperties()) { + const key = messageProp.getName(); + // Resolve the property type in the context of the declaration + const propType = messageProp.getTypeAtLocation(declaration); + if (propType.isStringLiteral()) { + messages[key] = propType.getLiteralValueOrThrow() as string; + } + } + + if (Object.keys(messages).length === 0) { + return undefined; + } + + return { id, messages }; +} + +/** + * Creates a ts-morph Project using the target repo's tsconfig.json. + */ +export function createTranslationProject(tsconfigPath: string): Project { + return new Project({ + tsConfigFilePath: tsconfigPath, + skipAddingFilesFromTsConfig: true, + }); +} diff --git a/packages/cli-module-translations/src/lib/messageFilePath.test.ts b/packages/cli-module-translations/src/lib/messageFilePath.test.ts new file mode 100644 index 0000000000..4c276e114c --- /dev/null +++ b/packages/cli-module-translations/src/lib/messageFilePath.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2026 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 { + formatMessagePath, + createMessagePathParser, + messagePatternToGlob, + patternHasSubdirectories, + DEFAULT_MESSAGE_PATTERN, +} from './messageFilePath'; + +describe('messageFilePath', () => { + describe('formatMessagePath', () => { + it('formats the default pattern', () => { + expect(formatMessagePath(DEFAULT_MESSAGE_PATTERN, 'org', 'en')).toBe( + 'messages/org.en.json', + ); + }); + + it('formats with a different language', () => { + expect(formatMessagePath(DEFAULT_MESSAGE_PATTERN, 'catalog', 'sv')).toBe( + 'messages/catalog.sv.json', + ); + }); + + it('formats a language-directory pattern', () => { + expect(formatMessagePath('{lang}/{id}.json', 'org', 'sv')).toBe( + 'sv/org.json', + ); + }); + + it('formats a pattern with lang first in filename', () => { + expect(formatMessagePath('{lang}.{id}.json', 'org', 'de')).toBe( + 'de.org.json', + ); + }); + }); + + describe('createMessagePathParser', () => { + it('parses the default pattern', () => { + const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); + expect(parse('messages/org.en.json')).toEqual({ id: 'org', lang: 'en' }); + }); + + it('parses dotted ref IDs in the default pattern', () => { + const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); + expect(parse('messages/plugin.notifications.sv.json')).toEqual({ + id: 'plugin.notifications', + lang: 'sv', + }); + }); + + it('parses a language-directory pattern', () => { + const parse = createMessagePathParser('{lang}/{id}.json'); + expect(parse('sv/org.json')).toEqual({ id: 'org', lang: 'sv' }); + }); + + it('returns undefined for non-matching paths', () => { + const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); + expect(parse('not-a-match.txt')).toBeUndefined(); + expect(parse('other/org.en.json')).toBeUndefined(); + }); + + it('returns undefined for invalid language code', () => { + const parse = createMessagePathParser('{lang}/{id}.json'); + expect(parse('123/org.json')).toBeUndefined(); + }); + + it('throws on pattern missing {id}', () => { + expect(() => createMessagePathParser('{lang}.json')).toThrow( + 'must contain {id}', + ); + }); + + it('throws on pattern missing {lang}', () => { + expect(() => createMessagePathParser('{id}.json')).toThrow( + 'must contain {lang}', + ); + }); + + it('throws on pattern not ending with .json', () => { + expect(() => createMessagePathParser('{id}.{lang}.yaml')).toThrow( + 'must end with .json', + ); + }); + }); + + describe('messagePatternToGlob', () => { + it('converts the default pattern', () => { + expect(messagePatternToGlob(DEFAULT_MESSAGE_PATTERN)).toBe( + 'messages/*.*.json', + ); + }); + + it('converts a language-directory pattern', () => { + expect(messagePatternToGlob('{lang}/{id}.json')).toBe('*/*.json'); + }); + }); + + describe('patternHasSubdirectories', () => { + it('returns true for the default pattern', () => { + expect(patternHasSubdirectories(DEFAULT_MESSAGE_PATTERN)).toBe(true); + }); + + it('returns true for patterns with directories', () => { + expect(patternHasSubdirectories('{lang}/{id}.json')).toBe(true); + }); + }); +}); diff --git a/packages/cli-module-translations/src/lib/messageFilePath.ts b/packages/cli-module-translations/src/lib/messageFilePath.ts new file mode 100644 index 0000000000..eb3dc8ea5c --- /dev/null +++ b/packages/cli-module-translations/src/lib/messageFilePath.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2026 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. + */ + +// The default language for exported translation messages. +export const DEFAULT_LANGUAGE = 'en'; + +// Default file path pattern for translation message files relative to the +// translations directory. Supported placeholders: {id} and {lang}. +export const DEFAULT_MESSAGE_PATTERN = 'messages/{id}.{lang}.json'; + +/** Formats a message file pattern into a concrete relative path. */ +export function formatMessagePath( + pattern: string, + id: string, + lang: string, +): string { + return pattern.replace(/\{id\}/g, id).replace(/\{lang\}/g, lang); +} + +/** Creates a parser that extracts id and lang from a relative file path. */ +export function createMessagePathParser( + pattern: string, +): (relativePath: string) => { id: string; lang: string } | undefined { + validatePattern(pattern); + + // Build a regex from the pattern by escaping special chars and replacing + // {id} and {lang} with named capture groups. + const escaped = pattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\\{id\\}/g, '(?[^/]+)') + .replace(/\\{lang\\}/g, '(?[a-z]{2})'); + + const regex = new RegExp(`^${escaped}$`); + + return (relPath: string) => { + const match = relPath.match(regex); + if (!match?.groups) { + return undefined; + } + return { id: match.groups.id, lang: match.groups.lang }; + }; +} + +/** Converts a message pattern into a glob string for discovering files. */ +export function messagePatternToGlob(pattern: string): string { + return pattern.replace(/\{id\}/g, '*').replace(/\{lang\}/g, '*'); +} + +/** Returns whether the pattern produces paths with subdirectories. */ +export function patternHasSubdirectories(pattern: string): boolean { + return pattern.includes('/'); +} + +export function validatePattern(pattern: string) { + if (!pattern.includes('{id}')) { + throw new Error( + `Invalid message file pattern: must contain {id} placeholder. Got: ${pattern}`, + ); + } + if (!pattern.includes('{lang}')) { + throw new Error( + `Invalid message file pattern: must contain {lang} placeholder. Got: ${pattern}`, + ); + } + if (!pattern.endsWith('.json')) { + throw new Error( + `Invalid message file pattern: must end with .json. Got: ${pattern}`, + ); + } +} diff --git a/packages/cli-node/.eslintrc.js b/packages/cli-node/.eslintrc.js index e2a53a6ad2..7e7c302633 100644 --- a/packages/cli-node/.eslintrc.js +++ b/packages/cli-node/.eslintrc.js @@ -1 +1,19 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +/* + * Copyright 2026 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. + */ +module.exports = { + ...require('@backstage/cli/config/eslint-factory')(__dirname), + ignorePatterns: ['config/**'], +}; diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index 3f62ca6a9a..96a404989e 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/cli-node +## 0.2.19-next.1 + +### Patch Changes + +- 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. +- 3c811bf: Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.2.19-next.0 + +### Patch Changes + +- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added support for the new `peerModules` metadata field in `package.json`. This field allows plugin packages to declare modules that should be installed alongside them for cross-plugin integrations. The field is validated by `backstage-cli repo fix --publish`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/cli-common@0.1.18 + ## 0.2.18-next.1 ### Patch Changes diff --git a/packages/cli-node/config/nodeTransform.cjs b/packages/cli-node/config/nodeTransform.cjs new file mode 100644 index 0000000000..15bcdba9bc --- /dev/null +++ b/packages/cli-node/config/nodeTransform.cjs @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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. + */ + +const { pathToFileURL } = require('node:url'); +const { transformSync } = require('@swc/core'); +const { addHook } = require('pirates'); +const { Module } = require('node:module'); + +// This hooks into module resolution and overrides imports of packages that +// exist in the linked workspace to instead be resolved from the linked workspace. +if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) { + const { join: joinPath } = require('node:path'); + const { getPackagesSync } = require('@manypkg/get-packages'); + const { packages: linkedPackages, root: linkedRoot } = getPackagesSync( + process.env.BACKSTAGE_CLI_LINKED_WORKSPACE, + ); + + // Matches all packages in the linked workspaces, as well as sub-path exports from them + const replacementRegex = new RegExp( + `^(?:${linkedPackages + .map(pkg => pkg.packageJson.name) + .join('|')})(?:/.*)?$`, + ); + + const origLoad = Module._load; + Module._load = function requireHook(request, parent) { + if (!replacementRegex.test(request)) { + return origLoad.call(this, request, parent); + } + + // The package import that we're overriding will always existing in the root + // node_modules of the linked workspace, so it's enough to override the + // parent paths with that single entry + return origLoad.call(this, request, { + ...parent, + paths: [joinPath(linkedRoot.dir, 'node_modules')], + }); + }; +} + +addHook( + (code, filename) => { + const transformed = transformSync(code, { + filename, + sourceMaps: 'inline', + module: { + type: 'commonjs', + ignoreDynamic: true, + }, + jsc: { + target: 'es2023', + parser: { + syntax: 'typescript', + }, + }, + }); + process.send?.({ type: 'watch', path: filename }); + return transformed.code; + }, + { extensions: ['.ts', '.cts'], ignoreNodeModules: true }, +); + +addHook( + (code, filename) => { + process.send?.({ type: 'watch', path: filename }); + return code; + }, + { extensions: ['.js', '.cjs'], ignoreNodeModules: true }, +); + +// Register module hooks, used by "type": "module" in package.json, .mjs and +// .mts files, as well as dynamic import(...)s, although dynamic imports will be +// handled be the CommonJS hooks in this file if what it points to is CommonJS. +Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename)); diff --git a/packages/cli-node/config/nodeTransformHooks.mjs b/packages/cli-node/config/nodeTransformHooks.mjs new file mode 100644 index 0000000000..592867e57a --- /dev/null +++ b/packages/cli-node/config/nodeTransformHooks.mjs @@ -0,0 +1,294 @@ +/* + * Copyright 2024 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 { dirname, extname, resolve as resolvePath } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { transformFile } from '@swc/core'; +import { isBuiltin } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; + +// @ts-check + +// No explicit file extension, no type in package.json +const DEFAULT_MODULE_FORMAT = 'commonjs'; + +// Source file extensions to look for when using bundle resolution strategy +const SRC_EXTS = ['.ts', '.js']; +const TS_EXTS = ['.ts', '.mts', '.cts']; +const moduleTypeTable = { + '.mjs': 'module', + '.mts': 'module', + '.cjs': 'commonjs', + '.cts': 'commonjs', + '.ts': undefined, + '.js': undefined, +}; + +/** @type {import('module').ResolveHook} */ +export async function resolve(specifier, context, nextResolve) { + // Built-in modules are handled by the default resolver + if (isBuiltin(specifier)) { + return nextResolve(specifier, context); + } + + const ext = extname(specifier); + + // Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below. + if (ext === '.json' && !context.importAttributes?.type) { + const jsonResult = await nextResolve(specifier, context); + return { + ...jsonResult, + format: 'commonjs', + importAttributes: { type: 'json' }, + }; + } + + // Anything else with an explicit extension is handled by the default + // resolver, except that we help determine the module type where needed. + if (ext !== '') { + return withDetectedModuleType(await nextResolve(specifier, context)); + } + + // Other external modules are handled by the default resolver, but again we + // help determine the module type where needed. + if (!specifier.startsWith('.')) { + return withDetectedModuleType(await nextResolve(specifier, context)); + } + + // The rest of this function handles the case of resolving imports that do not + // specify any extension and might point to a directory with an `index.*` + // file. We resolve those using the same logic as most JS bundlers would, with + // the addition of checking if there's an explicit module format listed in the + // closest `package.json` file. + // + // We use a bundle resolution strategy in order to keep code consistent across + // Backstage codebases that contains code both for Web and Node.js, and to + // support packages with common code that can be used in both environments. + try { + // This is expected to throw, but in the event that this module specifier is + // supported we prefer to use the default resolver. + return await nextResolve(specifier, context); + } catch (error) { + if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { + const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`; + const resolved = await resolveWithoutExt(spec, context, nextResolve); + if (resolved) { + return withDetectedModuleType(resolved); + } + } else if (error.code === 'ERR_MODULE_NOT_FOUND') { + const resolved = await resolveWithoutExt(specifier, context, nextResolve); + if (resolved) { + return withDetectedModuleType(resolved); + } + } + + // Unexpected error or no resolution found + throw error; + } +} + +/** + * Populates the `format` field in the resolved object based on the closest `package.json` file. + * + * @param {import('module').ResolveFnOutput} resolved + * @returns {Promise} + */ +async function withDetectedModuleType(resolved) { + // Already has an explicit format + if (resolved.format) { + return resolved; + } + // Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default. + if (resolved.format === null) { + return { ...resolved, format: DEFAULT_MODULE_FORMAT }; + } + + const ext = extname(resolved.url); + + const explicitFormat = moduleTypeTable[ext]; + if (explicitFormat) { + return { + ...resolved, + format: explicitFormat, + }; + } + + // Under normal circumstances .js files should reliably have a format and so + // we should only reach this point for .ts files. However, if additional + // custom loaders are being used the format may not be detected for .js files + // either. As such we don't restrict the file format at this point. + + // TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring + const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url)); + if (!packageJsonPath) { + return resolved; + } + + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); + return { + ...resolved, + format: packageJson.type ?? DEFAULT_MODULE_FORMAT, + }; +} + +/** + * Find the closest package.json file from the given path. + * + * TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable. + * @param {string} startPath + * @returns {Promise} + */ +async function findPackageJSON(startPath) { + let path = startPath; + + // Some confidence check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + if (existsSync(packagePath)) { + return packagePath; + } + + const newPath = dirname(path); + if (newPath === path) { + return undefined; + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for package.json at ${startPath}`, + ); +} + +/** @type {import('module').ResolveHook} */ +async function resolveWithoutExt(specifier, context, nextResolve) { + for (const tryExt of SRC_EXTS) { + try { + const resolved = await nextResolve(specifier + tryExt, { + ...context, + format: 'commonjs', + }); + return { + ...resolved, + format: moduleTypeTable[tryExt] ?? resolved.format, + }; + } catch { + /* ignore */ + } + } + return undefined; +} + +/** @type {import('module').LoadHook} */ +export async function load(url, context, nextLoad) { + // Non-file URLs are handled by the default loader + if (!url.startsWith('file://')) { + return nextLoad(url, context); + } + + // JSON files loaded as CommonJS are handled by this custom loader, because + // the default one doesn't work. For JSON loading to work we'd need the + // synchronous hooks that aren't supported yet, or avoid using the CommonJS + // compatibility. + if ( + context.format === 'commonjs' && + context.importAttributes?.type === 'json' + ) { + try { + // TODO(Rugvip): Make sure this is valid JSON + const content = await readFile(fileURLToPath(url), 'utf8'); + return { + source: `module.exports = (${content})`, + format: 'commonjs', + shortCircuit: true, + }; + } catch { + // Let the default loader generate the error + return nextLoad(url, context); + } + } + + const ext = extname(url); + + // Non-TS files are handled by the default loader + if (!TS_EXTS.includes(ext)) { + return nextLoad(url, context); + } + + const format = context.format ?? DEFAULT_MODULE_FORMAT; + + // We have two choices at this point, we can either transform CommonJS files + // and return the transformed source code, or let the default loader handle + // them. If we transform them ourselves we will enter CommonJS compatibility + // mode in the new module system in Node.js, this effectively means all + // CommonJS loaded via `require` calls from this point will all be treated as + // if it was loaded via `import` calls from modules. + // + // The CommonJS compatibility layer will try to identify named exports and + // make them available directly, which is convenient as it avoids things like + // `import(...).then(m => m.default.foo)`, allowing you to instead write + // `import(...).then(m => m.foo)`. The compatibility layer doesn't always work + // all that well though, and can lead to module loading issues in many cases, + // especially for older code. + + // This `if` block opts-out of using CommonJS compatibility mode by default, + // and instead leaves it to our existing loader to transform CommonJS. We do + // however use compatibility mode for the more explicit .cts file extension, + // allows for a way to opt-in to the new behavior. + // + // TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead + if (format === 'commonjs' && ext !== '.cts') { + return nextLoad(url, { ...context, format }); + } + + // If the Node.js version we're running supports TypeScript, i.e. type + // stripping, we hand over to the default loader. This is done for all cases + // except if we're loading a .ts file that's been resolved to CommonJS format. + // This is because these files aren't actually CommonJS in the Backstage build + // system, and need to be transformed to CommonJS. + if ( + format === 'module-typescript' || + (format === 'module-commonjs' && ext !== '.ts') + ) { + return nextLoad(url, { ...context, format }); + } + + const transformed = await transformFile(fileURLToPath(url), { + sourceMaps: 'inline', + module: { + type: format === 'module' ? 'es6' : 'commonjs', + ignoreDynamic: true, + + // This helps the Node.js CommonJS compat layer identify named exports. + exportInteropAnnotation: true, + }, + jsc: { + target: 'es2023', + parser: { + syntax: 'typescript', + }, + }, + }); + + return { + ...context, + shortCircuit: true, + source: transformed.code, + format, + responseURL: url, + }; +} diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 58cbe6855e..483b0eccff 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.18-next.1", + "version": "0.2.19-next.1", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" @@ -20,7 +20,8 @@ "main": "src/index.ts", "types": "src/index.ts", "files": [ - "dist" + "dist", + "config" ], "scripts": { "build": "backstage-cli package build", @@ -35,14 +36,28 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0", + "chalk": "^4.0.0", + "commander": "^12.0.0", "fs-extra": "^11.2.0", + "pirates": "^4.0.6", "semver": "^7.5.3", + "yaml": "^2.0.0", "zod": "^3.25.76" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/test-utils": "workspace:^" + "@backstage/test-utils": "workspace:^", + "@types/yarnpkg__lockfile": "^1.1.4" + }, + "peerDependencies": { + "@swc/core": "^1.15.6" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } } } diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 55d2443643..2fac82080b 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,12 +86,62 @@ export interface BackstagePackageJson { version: string; } +// @public +export interface CliCommand { + deprecated?: boolean; + description: string; + execute: + | ((context: CliCommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CliCommandContext) => Promise; + }>; + }; + experimental?: boolean; + path: string[]; +} + +// @public +export interface CliCommandContext { + args: string[]; + info: { + usage: string; + name: string; + }; +} + +// @public +export interface CliModule { + // (undocumented) + readonly $$type: '@backstage/CliModule'; +} + +// @public +export type ConcurrentTasksOptions = { + concurrencyFactor?: number; + items: Iterable; + worker: (item: TItem) => Promise; +}; + +// @public +export function createCliModule(options: { + packageJson: { + name: string; + }; + init: (registry: { + addCommand: (command: CliCommand) => void; + }) => Promise; +}): CliModule; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; static readFileAtRef(path: string, ref: string): Promise; } +// @public +export function hasBackstageYarnPlugin(workspaceDir?: string): Promise; + // @public export function isMonoRepo(): Promise; @@ -104,6 +154,7 @@ export class Lockfile { keys(): IterableIterator; static load(path: string): Promise; static parse(content: string): Lockfile; + toString(): string; } // @public @@ -176,6 +227,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' @@ -200,4 +252,45 @@ export class PackageRoles { static getRoleFromPackage(pkgJson: unknown): PackageRole | undefined; static getRoleInfo(role: string): PackageRoleInfo; } + +// @public +export function runCliModule(options: { + module: CliModule; + name: string; + version?: string; +}): Promise; + +// @public +export function runConcurrentTasks( + options: ConcurrentTasksOptions, +): Promise; + +// @public +export function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, +): Promise<{ + results: TResult[]; +}>; + +// @public +export class SuccessCache { + // (undocumented) + static create(options: { name: string; basePath?: string }): SuccessCache; + // (undocumented) + read(): Promise>; + static trimPaths(input: string): string; + // (undocumented) + write(newEntries: Iterable): Promise; +} + +// @public +export type WorkerQueueThreadsOptions = { + items: Iterable; + workerFactory: ( + context: TContext, + ) => + | ((item: TItem) => Promise) + | Promise<(item: TItem) => Promise>; + context?: TContext; +}; ``` diff --git a/packages/cli/src/lib/cache/SuccessCache.ts b/packages/cli-node/src/cache/SuccessCache.ts similarity index 83% rename from packages/cli/src/lib/cache/SuccessCache.ts rename to packages/cli-node/src/cache/SuccessCache.ts index a799970b4d..78913ed0b6 100644 --- a/packages/cli/src/lib/cache/SuccessCache.ts +++ b/packages/cli-node/src/cache/SuccessCache.ts @@ -16,12 +16,18 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; -import { paths } from '../paths'; +import { targetPaths } from '@backstage/cli-common'; const DEFAULT_CACHE_BASE_PATH = 'node_modules/.cache/backstage-cli'; const CACHE_MAX_AGE_MS = 7 * 24 * 3600_000; +/** + * A file-system-based cache that tracks successful operations by storing + * timestamped marker files. + * + * @public + */ export class SuccessCache { readonly #path: string; @@ -31,11 +37,18 @@ export class SuccessCache { * location. */ static trimPaths(input: string) { - return input.replaceAll(paths.targetRoot, ''); + return input.replaceAll(targetPaths.rootDir, ''); } - constructor(name: string, basePath?: string) { - this.#path = resolvePath(basePath ?? DEFAULT_CACHE_BASE_PATH, name); + static create(options: { name: string; basePath?: string }): SuccessCache { + return new SuccessCache(options); + } + + private constructor(options: { name: string; basePath?: string }) { + this.#path = resolvePath( + options.basePath ?? DEFAULT_CACHE_BASE_PATH, + options.name, + ); } async read(): Promise> { @@ -89,7 +102,6 @@ export class SuccessCache { const empty = Buffer.alloc(0); for (const key of newEntries) { - // Remove any existing items with the key we're about to add const trimmedItems = existingItems.filter(item => item.endsWith(`_${key}`), ); diff --git a/packages/cli-node/src/cache/index.ts b/packages/cli-node/src/cache/index.ts new file mode 100644 index 0000000000..53c9f0e4e4 --- /dev/null +++ b/packages/cli-node/src/cache/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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 { SuccessCache } from './SuccessCache'; diff --git a/packages/cli-node/src/cli-module/createCliModule.ts b/packages/cli-node/src/cli-module/createCliModule.ts new file mode 100644 index 0000000000..b2facc3a67 --- /dev/null +++ b/packages/cli-node/src/cli-module/createCliModule.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2024 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 { OpaqueCliModule } from '@internal/cli'; +import { CliCommand, CliModule } from './types'; + +/** + * Creates a new CLI plugin that provides commands to the Backstage CLI. + * + * The `init` callback is invoked immediately at creation time and is used + * to register commands via the provided registry. The commands are then + * made available to the CLI host once the returned promise resolves. + * + * @example + * ``` + * import { createCliModule } from '@backstage/cli-node'; + * import packageJson from '../package.json'; + * + * export default createCliModule({ + * packageJson, + * init: async reg => { + * reg.addCommand({ + * path: ['repo', 'test'], + * description: 'Run tests across the repository', + * execute: { loader: () => import('./commands/test') }, + * }); + * }, + * }); + * ``` + * + * @public + */ +export function createCliModule(options: { + /** The `package.json` contents of the plugin package, used to identify the plugin. */ + packageJson: { name: string }; + /** + * An initialization callback that registers commands with the CLI. + * Called immediately when the plugin is created. + */ + init: (registry: { + /** Registers a new command with the CLI. */ + addCommand: (command: CliCommand) => void; + }) => Promise; +}): CliModule { + if (!options.packageJson.name) { + throw new Error( + 'The packageJson provided to createCliModule must have a name', + ); + } + + const commands: CliCommand[] = []; + const commandsPromise = Promise.resolve() + .then(() => options.init({ addCommand: command => commands.push(command) })) + .then(() => commands); + + return OpaqueCliModule.createInstance('v1', { + packageName: options.packageJson.name, + commands: commandsPromise, + }); +} diff --git a/packages/cli-node/src/cli-module/index.ts b/packages/cli-node/src/cli-module/index.ts new file mode 100644 index 0000000000..56324c3423 --- /dev/null +++ b/packages/cli-node/src/cli-module/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2024 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 { createCliModule } from './createCliModule'; +export { runCliModule } from './runCliModule'; +export type { CliCommand, CliCommandContext, CliModule } from './types'; diff --git a/packages/cli-node/src/cli-module/runCliModule.ts b/packages/cli-node/src/cli-module/runCliModule.ts new file mode 100644 index 0000000000..0dc40e4c33 --- /dev/null +++ b/packages/cli-node/src/cli-module/runCliModule.ts @@ -0,0 +1,222 @@ +/* + * Copyright 2024 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 { + OpaqueCliModule, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from '@internal/cli'; +import type { CommandNode } from '@internal/cli'; +import { Command } from 'commander'; +import chalk from 'chalk'; +import { isError, stringifyError } from '@backstage/errors'; +import type { CliModule, CliCommand } from './types'; + +function buildCommandGraph(commands: ReadonlyArray): CommandNode[] { + const graph: CommandNode[] = []; + + for (const command of commands) { + const { path } = command; + let current = graph; + + for (let i = 0; i < path.length - 1; i++) { + const name = path[i]; + let next = current.find( + n => + OpaqueCommandTreeNode.isType(n) && + OpaqueCommandTreeNode.toInternal(n).name === name, + ); + if (!next) { + next = OpaqueCommandTreeNode.createInstance('v1', { + name, + children: [], + }); + current.push(next); + } + current = OpaqueCommandTreeNode.toInternal(next).children; + } + + current.push( + OpaqueCommandLeafNode.createInstance('v1', { + name: path[path.length - 1], + command, + }), + ); + } + + return graph; +} + +function exitWithError(error: unknown): never { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit( + isError(error) && 'code' in error && typeof error.code === 'number' + ? error.code + : 1, + ); +} + +function registerCommands( + graph: CommandNode[], + program: Command, + programName: string, +): void { + const queue = graph.map(node => ({ node, argParser: program })); + + while (queue.length) { + const { node, argParser } = queue.shift()!; + + if (OpaqueCommandTreeNode.isType(node)) { + const internal = OpaqueCommandTreeNode.toInternal(node); + const treeParser = argParser + .command(`${internal.name} [command]`, { + hidden: isCommandNodeHidden(node), + }) + .description(internal.name); + + queue.push( + ...internal.children.map(child => ({ + node: child, + argParser: treeParser, + })), + ); + } else { + const internal = OpaqueCommandLeafNode.toInternal(node); + argParser + .command(internal.name, { + hidden: + !!internal.command.deprecated || !!internal.command.experimental, + }) + .description(internal.command.description) + .helpOption(false) + .allowUnknownOption(true) + .allowExcessArguments(true) + .action(async () => { + try { + const args = program.parseOptions(process.argv); + + const nonProcessArgs = args.operands.slice(2); + const positionalArgs = []; + let index = 0; + for ( + let argIndex = 0; + argIndex < nonProcessArgs.length; + argIndex++ + ) { + if ( + argIndex === index && + internal.command.path[argIndex] === nonProcessArgs[argIndex] + ) { + index += 1; + continue; + } + positionalArgs.push(nonProcessArgs[argIndex]); + } + const context = { + args: [...positionalArgs, ...args.unknown], + info: { + usage: [programName, ...internal.command.path].join(' '), + name: internal.command.path.join(' '), + }, + }; + + if (typeof internal.command.execute === 'function') { + await internal.command.execute(context); + } else { + const mod = await internal.command.execute.loader(); + const fn = + typeof mod.default === 'function' + ? mod.default + : (mod.default as any).default; + await fn(context); + } + process.exit(0); + } catch (error: unknown) { + exitWithError(error); + } + }); + } + } +} + +/** + * Runs a CLI module as a standalone program. + * + * This helper extracts the commands from a {@link CliModule} and exposes + * them as a fully functional CLI with help output and argument parsing. + * It is intended to be called from a module package's `bin` entry point + * so that the module can be executed directly without being wired into + * a larger CLI host. + * + * @example + * ```ts + * #!/usr/bin/env node + * import { runCliModule } from '@backstage/cli-node'; + * import cliModule from './index'; + * + * runCliModule({ + * module: cliModule, + * name: 'backstage-auth', + * version: require('../package.json').version, + * }); + * ``` + * + * @public + */ +export async function runCliModule(options: { + /** The CLI module to run. */ + module: CliModule; + /** The program name shown in help output and usage strings. */ + name: string; + /** The version string shown when `--version` is passed. */ + version?: string; +}): Promise { + const { module: cliModule, name, version } = options; + + if (!OpaqueCliModule.isType(cliModule)) { + throw new Error( + `Invalid CLI module: expected a module created with createCliModule`, + ); + } + + const internal = OpaqueCliModule.toInternal(cliModule); + const commands = await internal.commands; + const graph = buildCommandGraph(commands); + + const program = new Command(); + program.name(name).allowUnknownOption(true).allowExcessArguments(true); + + if (version) { + program.version(version); + } + + registerCommands(graph, program, name); + + program.on('command:*', () => { + console.log(); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); + console.log(); + program.outputHelp(); + process.exit(1); + }); + + process.on('unhandledRejection', rejection => { + exitWithError(rejection); + }); + + await program.parseAsync(process.argv); +} diff --git a/packages/cli-node/src/cli-module/types.ts b/packages/cli-node/src/cli-module/types.ts new file mode 100644 index 0000000000..3db290f89f --- /dev/null +++ b/packages/cli-node/src/cli-module/types.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2024 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. + */ + +/** + * The context provided to a CLI command at the time of execution. + * + * Contains the parsed arguments and metadata about the command being run. + * + * @public + */ +export interface CliCommandContext { + /** + * The remaining arguments passed to the command after the command path + * has been resolved. This includes both positional arguments and flags. + * + * For example, running `backstage-cli repo test --verbose src/` would + * result in `args` being `['--verbose', 'src/']`. + */ + args: string[]; + /** + * Metadata about the command being executed. + */ + info: { + /** + * The full usage string of the command including the program name, + * for example `"backstage-cli repo test"`. + */ + usage: string; + /** + * The name of the command as defined by its path, + * for example `"repo test"`. + */ + name: string; + }; +} + +/** + * A command definition for a Backstage CLI plugin. + * + * Each command is identified by a `path` that determines its position in + * the command tree. For example, a path of `['repo', 'test']` registers + * the command as `backstage-cli repo test`. + * + * Commands can either provide an `execute` function directly, or use a + * `loader` for deferred loading of the implementation. The loader pattern + * is recommended for commands with heavy dependencies, as it avoids + * loading the implementation until the command is actually invoked. + * + * @public + */ +export interface CliCommand { + /** + * The path segments that define the command's position in the CLI tree. + * For example, `['repo', 'test']` maps to `backstage-cli repo test`. + */ + path: string[]; + /** + * A short description of the command, displayed in help output. + */ + description: string; + /** + * If `true`, the command is deprecated and will be hidden from help output + * but can still be invoked. + */ + deprecated?: boolean; + /** + * If `true`, the command is experimental and will be hidden from help + * output but can still be invoked. + */ + experimental?: boolean; + /** + * The command implementation, either as a direct function or as a loader + * that returns the implementation as a default export. The loader form + * is useful for deferring heavy imports until the command is invoked. + * + * @example + * Direct execution: + * ``` + * execute: async ({ args }) => { ... } + * ``` + * + * @example + * Deferred loading: + * ``` + * execute: { loader: () => import('./my-command') } + * ``` + */ + execute: + | ((context: CliCommandContext) => Promise) + | { + loader: () => Promise<{ + default: (context: CliCommandContext) => Promise; + }>; + }; +} + +/** + * An opaque representation of a Backstage CLI plugin, created + * using {@link createCliModule}. + * + * @public + */ +export interface CliModule { + readonly $$type: '@backstage/CliModule'; +} diff --git a/packages/cli-node/src/concurrency/concurrency.ts b/packages/cli-node/src/concurrency/concurrency.ts new file mode 100644 index 0000000000..fa46f132a0 --- /dev/null +++ b/packages/cli-node/src/concurrency/concurrency.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2020 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 os from 'node:os'; + +const defaultConcurrency = Math.max(Math.ceil(os.cpus().length / 2), 1); + +const CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_CONCURRENCY'; +const DEPRECATED_CONCURRENCY_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; + +type ConcurrencyOption = boolean | string | number | null | undefined; + +function parseConcurrencyOption(value: ConcurrencyOption): number { + if (value === undefined || value === null) { + return defaultConcurrency; + } else if (typeof value === 'boolean') { + return value ? defaultConcurrency : 1; + } else if (typeof value === 'number' && Number.isInteger(value)) { + if (value < 1) { + return 1; + } + return value; + } else if (typeof value === 'string') { + if (value === 'true') { + return parseConcurrencyOption(true); + } else if (value === 'false') { + return parseConcurrencyOption(false); + } + const parsed = Number(value); + if (Number.isInteger(parsed)) { + return parseConcurrencyOption(parsed); + } + } + + throw Error( + `Concurrency option value '${value}' is not a boolean or integer`, + ); +} + +let hasWarnedDeprecation = false; + +/** @internal */ +export function getEnvironmentConcurrency() { + if (process.env[CONCURRENCY_ENV_VAR] !== undefined) { + return parseConcurrencyOption(process.env[CONCURRENCY_ENV_VAR]); + } + if (process.env[DEPRECATED_CONCURRENCY_ENV_VAR] !== undefined) { + if (!hasWarnedDeprecation) { + hasWarnedDeprecation = true; + console.warn( + `The ${DEPRECATED_CONCURRENCY_ENV_VAR} environment variable is deprecated, use ${CONCURRENCY_ENV_VAR} instead`, + ); + } + return parseConcurrencyOption(process.env[DEPRECATED_CONCURRENCY_ENV_VAR]); + } + return defaultConcurrency; +} diff --git a/packages/cli/src/modules/maintenance/commands/package/clean.ts b/packages/cli-node/src/concurrency/index.ts similarity index 69% rename from packages/cli/src/modules/maintenance/commands/package/clean.ts rename to packages/cli-node/src/concurrency/index.ts index 5e2d0d65b3..3ff4067a20 100644 --- a/packages/cli/src/modules/maintenance/commands/package/clean.ts +++ b/packages/cli-node/src/concurrency/index.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { paths } from '../../../../lib/paths'; - -export default async function clean() { - await fs.remove(paths.resolveTarget('dist')); - await fs.remove(paths.resolveTarget('dist-types')); - await fs.remove(paths.resolveTarget('coverage')); -} +export type { ConcurrentTasksOptions } from './runConcurrentTasks'; +export { runConcurrentTasks } from './runConcurrentTasks'; +export type { WorkerQueueThreadsOptions } from './runWorkerQueueThreads'; +export { runWorkerQueueThreads } from './runWorkerQueueThreads'; diff --git a/packages/cli-node/src/concurrency/runConcurrentTasks.test.ts b/packages/cli-node/src/concurrency/runConcurrentTasks.test.ts new file mode 100644 index 0000000000..382a85d297 --- /dev/null +++ b/packages/cli-node/src/concurrency/runConcurrentTasks.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2020 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 { runConcurrentTasks } from './runConcurrentTasks'; + +describe('runConcurrentTasks', () => { + afterEach(() => { + delete process.env.BACKSTAGE_CLI_CONCURRENCY; + }); + + it('executes work in parallel', async () => { + const started = new Array(); + const done = new Array(); + const waiting = new Array<() => void>(); + + process.env.BACKSTAGE_CLI_CONCURRENCY = '4'; + const work = runConcurrentTasks({ + items: [0, 1, 2, 3, 4], + concurrencyFactor: 0.5, // 2 at a time + worker: async item => { + started.push(item); + await new Promise(resolve => { + waiting[item] = resolve; + }); + done.push(item); + }, + }); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1]); + expect(done).toEqual([]); + waiting[0](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2]); + expect(done).toEqual([0]); + waiting[1](); + waiting[2](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3, 4]); + expect(done).toEqual([0, 1, 2]); + waiting[3](); + waiting[4](); + + await work; + expect(done).toEqual([0, 1, 2, 3, 4]); + }); + + it('executes work sequentially', async () => { + const started = new Array(); + const done = new Array(); + const waiting = new Array<() => void>(); + + const work = runConcurrentTasks({ + items: [0, 1, 2, 3, 4], + concurrencyFactor: 0, // 1 at a time + worker: async item => { + started.push(item); + await new Promise(resolve => { + waiting[item] = resolve; + }); + done.push(item); + }, + }); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0]); + expect(done).toEqual([]); + waiting[0](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1]); + expect(done).toEqual([0]); + waiting[1](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2]); + waiting[2](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3]); + waiting[3](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3, 4]); + waiting[4](); + + await work; + expect(done).toEqual([0, 1, 2, 3, 4]); + }); + + it('returns void', async () => { + const result = await runConcurrentTasks({ + items: [1, 2, 3], + worker: async () => {}, + }); + expect(result).toBeUndefined(); + }); + + it('defaults to environment concurrency', async () => { + const started = new Array(); + process.env.BACKSTAGE_CLI_CONCURRENCY = '2'; + + await runConcurrentTasks({ + items: [0, 1], + worker: async item => { + started.push(item); + }, + }); + + expect(started).toEqual([0, 1]); + }); + + it('supports the deprecated BACKSTAGE_CLI_BUILD_PARALLEL with a warning', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2'; + await runConcurrentTasks({ + items: [0, 1], + worker: async () => {}, + }); + + expect(warnSpy).toHaveBeenCalledWith( + 'The BACKSTAGE_CLI_BUILD_PARALLEL environment variable is deprecated, use BACKSTAGE_CLI_CONCURRENCY instead', + ); + + delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + warnSpy.mockRestore(); + }); +}); diff --git a/packages/cli-node/src/concurrency/runConcurrentTasks.ts b/packages/cli-node/src/concurrency/runConcurrentTasks.ts new file mode 100644 index 0000000000..1695f25403 --- /dev/null +++ b/packages/cli-node/src/concurrency/runConcurrentTasks.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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 { getEnvironmentConcurrency } from './concurrency'; + +/** + * Options for {@link runConcurrentTasks}. + * + * @public + */ +export type ConcurrentTasksOptions = { + /** + * Decides the number of concurrent workers by multiplying + * this with the configured concurrency. + * + * Defaults to 1. + */ + concurrencyFactor?: number; + items: Iterable; + worker: (item: TItem) => Promise; +}; + +/** + * Runs items through a worker function concurrently across multiple async workers. + * + * @public + */ +export async function runConcurrentTasks( + options: ConcurrentTasksOptions, +): Promise { + const { concurrencyFactor = 1, items, worker } = options; + const concurrency = getEnvironmentConcurrency(); + + const sharedIterator = items[Symbol.iterator](); + const sharedIterable = { + [Symbol.iterator]: () => sharedIterator, + }; + + const workerCount = Math.max(Math.floor(concurrencyFactor * concurrency), 1); + await Promise.all( + Array(workerCount) + .fill(0) + .map(async () => { + for (const value of sharedIterable) { + await worker(value); + } + }), + ); +} diff --git a/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts new file mode 100644 index 0000000000..5dd2cda438 --- /dev/null +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 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 { runWorkerQueueThreads } from './runWorkerQueueThreads'; + +describe('runWorkerQueueThreads', () => { + it('should execute work in parallel', async () => { + const sharedData = new SharedArrayBuffer(10); + const sharedView = new Uint8Array(sharedData); + + const { results } = await runWorkerQueueThreads({ + context: sharedData, + items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + workerFactory: (data: SharedArrayBuffer) => { + const view = new Uint8Array(data); + + return async (i: number) => { + view[i] = 10 + i; + return 20 + i; + }; + }, + }); + + expect(Array.from(sharedView)).toEqual([ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + ]); + expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); + }); +}); diff --git a/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts new file mode 100644 index 0000000000..2d2a3b37e4 --- /dev/null +++ b/packages/cli-node/src/concurrency/runWorkerQueueThreads.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2020 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 { ErrorLike } from '@backstage/errors'; +import { Worker } from 'node:worker_threads'; +import { getEnvironmentConcurrency } from './concurrency'; + +type WorkerThreadMessage = + | { + type: 'done'; + } + | { + type: 'item'; + index: number; + item: unknown; + } + | { + type: 'start'; + } + | { + type: 'result'; + index: number; + result: unknown; + } + | { + type: 'error'; + error: ErrorLike; + }; + +/** + * Options for {@link runWorkerQueueThreads}. + * + * @public + */ +export type WorkerQueueThreadsOptions = { + /** The items to process */ + items: Iterable; + /** + * A function that will be called within each worker thread at startup, + * which should return the worker function that will be called for each item. + * + * This function must be defined as an arrow function or using the + * function keyword, and must be entirely self contained, not referencing + * any variables outside of its scope. This is because the function source + * is stringified and evaluated in the worker thread. + * + * To pass data to the worker, use the `context` option and `items`, but + * note that they are both copied by value into the worker thread, except for + * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. + */ + workerFactory: ( + context: TContext, + ) => + | ((item: TItem) => Promise) + | Promise<(item: TItem) => Promise>; + /** Context data supplied to each worker factory */ + context?: TContext; +}; + +/** + * Spawns one or more worker threads using the `worker_threads` module. + * Each thread processes one item at a time from the provided `options.items`. + * + * @public + */ +export async function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, +): Promise<{ results: TResult[] }> { + const items = Array.from(options.items); + const workerFactory = options.workerFactory; + const workerData = options.context; + const threadCount = Math.min(getEnvironmentConcurrency(), items.length); + + const iterator = items[Symbol.iterator](); + const results = new Array(); + let itemIndex = 0; + + await Promise.all( + Array(threadCount) + .fill(0) + .map(async () => { + const thread = new Worker(`(${workerQueueThread})(${workerFactory})`, { + eval: true, + workerData, + }); + + return new Promise((resolve, reject) => { + thread.on('message', (message: WorkerThreadMessage) => { + if (message.type === 'start' || message.type === 'result') { + if (message.type === 'result') { + results[message.index] = message.result as TResult; + } + const { value, done } = iterator.next(); + if (done) { + thread.postMessage({ type: 'done' }); + } else { + thread.postMessage({ + type: 'item', + index: itemIndex, + item: value, + }); + itemIndex += 1; + } + } else if (message.type === 'error') { + const error = new Error(message.error.message); + error.name = message.error.name; + error.stack = message.error.stack; + reject(error); + } + }); + + thread.on('error', reject); + thread.on('exit', (code: number) => { + if (code !== 0) { + reject(new Error(`Worker thread exited with code ${code}`)); + } else { + resolve(); + } + }); + }); + }), + ); + + return { results }; +} + +/* istanbul ignore next */ +function workerQueueThread( + workerFuncFactory: ( + data: unknown, + ) => Promise<(item: unknown) => Promise>, +) { + const { parentPort, workerData } = require('node:worker_threads'); + + Promise.resolve() + .then(() => workerFuncFactory(workerData)) + .then( + workerFunc => { + parentPort.on('message', async (message: WorkerThreadMessage) => { + if (message.type === 'done') { + parentPort.close(); + return; + } + if (message.type === 'item') { + try { + const result = await workerFunc(message.item); + parentPort.postMessage({ + type: 'result', + index: message.index, + result, + }); + } catch (error) { + parentPort.postMessage({ type: 'error', error }); + } + } + }); + + parentPort.postMessage({ type: 'start' }); + }, + error => parentPort.postMessage({ type: 'error', error }), + ); +} diff --git a/packages/cli-node/src/git/GitUtils.ts b/packages/cli-node/src/git/GitUtils.ts index 7edb3581d6..f2350be309 100644 --- a/packages/cli-node/src/git/GitUtils.ts +++ b/packages/cli-node/src/git/GitUtils.ts @@ -15,7 +15,7 @@ */ import { assertError, ForwardedError } from '@backstage/errors'; -import { paths } from '../paths'; +import { targetPaths } from '@backstage/cli-common'; import { runOutput } from '@backstage/cli-common'; /** @@ -24,7 +24,7 @@ import { runOutput } from '@backstage/cli-common'; export async function runGit(...args: string[]) { try { const stdout = await runOutput(['git', ...args], { - cwd: paths.targetRoot, + cwd: targetPaths.rootDir, }); return stdout.trim().split(/\r\n|\r|\n/); } catch (error) { @@ -88,7 +88,7 @@ export class GitUtils { } const stdout = await runOutput(['git', 'show', `${showRef}:${path}`], { - cwd: paths.targetRoot, + cwd: targetPaths.rootDir, }); return stdout; } diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 1d3f3ec2ed..8831753a82 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -20,6 +20,10 @@ * @packageDocumentation */ +export * from './cache'; +export * from './cli-module'; +export * from './concurrency'; export * from './git'; export * from './monorepo'; export * from './roles'; +export * from './yarn'; diff --git a/packages/cli-node/src/monorepo/Lockfile.test.ts b/packages/cli-node/src/monorepo/Lockfile.test.ts index 03b50fea90..c2d35f7352 100644 --- a/packages/cli-node/src/monorepo/Lockfile.test.ts +++ b/packages/cli-node/src/monorepo/Lockfile.test.ts @@ -15,6 +15,7 @@ */ import { Lockfile } from './Lockfile'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 @@ -29,7 +30,74 @@ __metadata: cacheKey: 8 `; -describe('New Lockfile', () => { +const mockLegacy = `${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x: + version "2.0.1" + +b@^2: + version "2.0.0" +`; + +const mockModern = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 2.0.1 + +b@^2: + version: 2.0.0 +`; + +describe('Lockfile', () => { + const mockDir = createMockDirectory(); + + it('should load and serialize a legacy lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockLegacy, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, + ]); + expect(lockfile.toString()).toBe(mockLegacy); + }); + + it('should load and serialize a modern lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockModern, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, + ]); + expect(lockfile.toString()).toBe(mockModern); + }); +}); + +describe('Lockfile advanced', () => { describe('diff', () => { const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER} a@^1: diff --git a/packages/cli-node/src/monorepo/Lockfile.ts b/packages/cli-node/src/monorepo/Lockfile.ts index ff624191af..65e22dcf13 100644 --- a/packages/cli-node/src/monorepo/Lockfile.ts +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -14,12 +14,22 @@ * limitations under the License. */ -import { parseSyml } from '@yarnpkg/parsers'; +import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; +import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; import crypto from 'node:crypto'; import fs from 'fs-extra'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 +const NEW_HEADER = `${[ + `# This file is generated by running "yarn install" inside your project.\n`, + `# Manual changes might be lost - proceed with caution!\n`, +].join(``)}\n`; + +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 +const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; + /** @internal */ type LockfileData = { [entry: string]: { @@ -97,6 +107,8 @@ export class Lockfile { * @public */ static parse(content: string): Lockfile { + const legacy = LEGACY_REGEX.test(content); + let data: LockfileData; try { data = parseSyml(content); @@ -130,18 +142,21 @@ export class Lockfile { } } - return new Lockfile(packages, data); + return new Lockfile(packages, data, legacy); } private readonly packages: Map; private readonly data: LockfileData; + private readonly legacy: boolean; private constructor( packages: Map, data: LockfileData, + legacy: boolean = false, ) { this.packages = packages; this.data = data; + this.legacy = legacy; } /** Returns the name of all packages available in the lockfile */ @@ -154,6 +169,15 @@ export class Lockfile { return this.packages.keys(); } + /** + * Serialize the lockfile back to a string. + */ + toString(): string { + return this.legacy + ? legacyStringifyLockfile(this.data) + : NEW_HEADER + stringifySyml(this.data); + } + /** * Creates a simplified dependency graph from the lockfile data, where each * key is a package, and the value is a set of all packages that it depends on diff --git a/packages/cli-node/src/monorepo/PackageGraph.test.ts b/packages/cli-node/src/monorepo/PackageGraph.test.ts index d1a0cce649..51d8f7b7be 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.test.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.test.ts @@ -14,21 +14,16 @@ * limitations under the License. */ -import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from './PackageGraph'; import { Lockfile } from './Lockfile'; import { GitUtils } from '../git'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; const mockListChangedFiles = jest.spyOn(GitUtils, 'listChangedFiles'); const mockReadFileAtRef = jest.spyOn(GitUtils, 'readFileAtRef'); -jest.mock('../paths', () => ({ - paths: { - targetRoot: '/', - resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths), - }, -})); +overrideTargetPaths('/'); const testPackages = [ { diff --git a/packages/cli-node/src/monorepo/PackageGraph.ts b/packages/cli-node/src/monorepo/PackageGraph.ts index 6d3da7cf82..9d1745c7c5 100644 --- a/packages/cli-node/src/monorepo/PackageGraph.ts +++ b/packages/cli-node/src/monorepo/PackageGraph.ts @@ -16,7 +16,7 @@ import path from 'node:path'; import { getPackages, Package } from '@manypkg/get-packages'; -import { paths } from '../paths'; +import { targetPaths } from '@backstage/cli-common'; import { PackageRole } from '../roles'; import { GitUtils } from '../git'; import { Lockfile } from './Lockfile'; @@ -192,7 +192,7 @@ export class PackageGraph extends Map { * Lists all local packages in a monorepo. */ static async listTargetPackages(): Promise { - const { packages } = await getPackages(paths.targetDir); + const { packages } = await getPackages(targetPaths.dir); return packages as BackstagePackage[]; } @@ -332,7 +332,7 @@ export class PackageGraph extends Map { Array.from(this.values()).map(pkg => [ // relative from root, convert to posix, and add a / at the end path - .relative(paths.targetRoot, pkg.dir) + .relative(targetPaths.rootDir, pkg.dir) .split(path.sep) .join(path.posix.sep) + path.posix.sep, pkg, @@ -374,7 +374,7 @@ export class PackageGraph extends Map { let otherLockfile: Lockfile; try { thisLockfile = await Lockfile.load( - paths.resolveTargetRoot('yarn.lock'), + targetPaths.resolveRoot('yarn.lock'), ); otherLockfile = Lockfile.parse( await GitUtils.readFileAtRef('yarn.lock', options.ref), diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index da78c8dd53..ca6b209a7a 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -14,19 +14,22 @@ * limitations under the License. */ -import { paths } from '../paths'; +import { targetPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; /** - * Returns try if the current project is a monorepo. + * Returns true if the current project is a monorepo. + * + * Uses a simple presence check on the `workspaces` field. Empty or invalid + * workspace config is treated as a monorepo; we do not validate patterns. * * @public */ export async function isMonoRepo(): Promise { - const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); + const rootPackageJsonPath = targetPaths.resolveRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return Boolean(pkg?.workspaces?.packages); + return Boolean(pkg?.workspaces); } catch (error) { return false; } diff --git a/packages/cli-node/src/monorepo/isMonorepo.test.ts b/packages/cli-node/src/monorepo/isMonorepo.test.ts index de0dae0893..e13f9a8784 100644 --- a/packages/cli-node/src/monorepo/isMonorepo.test.ts +++ b/packages/cli-node/src/monorepo/isMonorepo.test.ts @@ -16,12 +16,10 @@ import { isMonoRepo } from './isMonoRepo'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; const mockDir = createMockDirectory(); - -jest.mock('../paths', () => ({ - paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, -})); +overrideTargetPaths(mockDir.path); describe('isMonoRepo', () => { it('should detect a monorepo', async () => { diff --git a/packages/cli-node/src/pacman/PackageManager.test.ts b/packages/cli-node/src/pacman/PackageManager.test.ts index dd35e74902..4f7446901f 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -15,16 +15,13 @@ */ import { createMockDirectory } from '@backstage/backend-test-utils'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import { detectPackageManager } from './PackageManager'; import { Yarn } from './yarn'; import { withLogCollector } from '@backstage/test-utils'; const mockDir = createMockDirectory(); - -jest.mock('../paths', () => ({ - ...jest.requireActual('../paths'), - paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, -})); +overrideTargetPaths(mockDir.path); const mockYarnCreate = jest.spyOn(Yarn, 'create'); @@ -55,6 +52,16 @@ describe('PackageManager', () => { await detectPackageManager(); expect(mockYarnCreate).toHaveBeenCalled(); }); + it('should detect via root package.json workspaces (array form)', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + name: 'foo', + workspaces: ['packages/*'], + }), + }); + await detectPackageManager(); + expect(mockYarnCreate).toHaveBeenCalled(); + }); it('should detect via root package.json packageManager', async () => { mockDir.setContent({ diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index 44c0849705..852ccc1288 100644 --- a/packages/cli-node/src/pacman/PackageManager.ts +++ b/packages/cli-node/src/pacman/PackageManager.ts @@ -16,7 +16,7 @@ import { Yarn } from './yarn'; import { Lockfile } from './Lockfile'; -import { paths } from '../paths'; +import { targetPaths } from '@backstage/cli-common'; import { RunOptions } from '@backstage/cli-common'; import fs from 'fs-extra'; @@ -49,12 +49,6 @@ export interface PackageManager { /** The file name of the lockfile used by the package manager. */ lockfileName(): string; - /** - * If this repo is a monorepo, returns the patterns specified by the package - * manager's monorepo configuration. Does not attempt to resolve any globs. - */ - getMonorepoPackages(): Promise; - /** Uses the package manager to run a command in the repo. */ run(args: string[], options?: RunOptions): Promise; @@ -91,7 +85,7 @@ export interface PackageManager { */ export async function detectPackageManager(): Promise { const hasYarnLockfile = await fileExists( - paths.resolveTargetRoot('yarn.lock'), + targetPaths.resolveRoot('yarn.lock'), ); if (hasYarnLockfile) { return await Yarn.create(); @@ -99,7 +93,7 @@ export async function detectPackageManager(): Promise { try { const packageJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), + targetPaths.resolveRoot('package.json'), ); if (packageJson.workspaces) { // technically this could be NPM as well diff --git a/packages/cli-node/src/pacman/yarn/Yarn.test.ts b/packages/cli-node/src/pacman/yarn/Yarn.test.ts deleted file mode 100644 index 9bf866b821..0000000000 --- a/packages/cli-node/src/pacman/yarn/Yarn.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2024 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 { createMockDirectory } from '@backstage/backend-test-utils'; -import { Yarn } from './Yarn'; - -const mockDir = createMockDirectory(); - -jest.mock('../../paths', () => ({ - ...jest.requireActual('../../paths'), - paths: { resolveTargetRoot: (...args: string[]) => mockDir.resolve(...args) }, -})); - -const yarnClassic = new Yarn({ version: '1.0.0', codename: 'classic' }); -const yarnBerry = new Yarn({ version: '3.0.0', codename: 'berry' }); -const allYarnVersions = [yarnClassic, yarnBerry]; - -describe('Yarn', () => { - describe.each(allYarnVersions)('%s.getMonorepoPackages', yarn => { - it('should detect a monorepo', async () => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'foo', - workspaces: { - packages: ['packages/*'], - }, - }), - }); - await expect(yarn.getMonorepoPackages()).resolves.toEqual(['packages/*']); - }); - - it('should detect a non-monorepo', async () => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'foo', - }), - }); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - - it('should return false if package.json is missing', async () => { - mockDir.setContent({}); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - }); -}); diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index a6544344ca..7aee7bdebf 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -22,8 +22,6 @@ import { import { PackageInfo, PackageManager } from '../PackageManager'; import { Lockfile } from '../Lockfile'; import { YarnVersion } from './types'; -import fs from 'fs-extra'; -import { paths } from '../../paths'; import { run, runOutput, RunOptions } from '@backstage/cli-common'; export class Yarn implements PackageManager { @@ -46,16 +44,6 @@ export class Yarn implements PackageManager { return 'yarn.lock'; } - async getMonorepoPackages() { - const rootPackageJsonPath = paths.resolveTargetRoot('package.json'); - try { - const pkg = await fs.readJson(rootPackageJsonPath); - return pkg?.workspaces?.packages || []; - } catch (error) { - return []; - } - } - async pack(out: string, packageDir: string) { const outArg = this.yarnVersion.codename === 'classic' ? '--filename' : '--out'; diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 70b62e2479..7761b0d4cc 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -33,6 +33,11 @@ const packageRoleInfos: PackageRoleInfo[] = [ platform: 'node', output: ['cjs'], }, + { + role: 'cli-module', + platform: 'node', + output: ['types', 'cjs'], + }, { role: 'web-library', platform: 'web', diff --git a/packages/cli-node/src/roles/types.ts b/packages/cli-node/src/roles/types.ts index d6eff04541..d0eaedbd02 100644 --- a/packages/cli-node/src/roles/types.ts +++ b/packages/cli-node/src/roles/types.ts @@ -23,6 +23,7 @@ export type PackageRole = | 'frontend' | 'backend' | 'cli' + | 'cli-module' | 'web-library' | 'node-library' | 'common-library' diff --git a/packages/cli/src/lib/paths.ts b/packages/cli-node/src/yarn/index.ts similarity index 80% rename from packages/cli/src/lib/paths.ts rename to packages/cli-node/src/yarn/index.ts index 2c658c27b3..871aa14ca8 100644 --- a/packages/cli/src/lib/paths.ts +++ b/packages/cli-node/src/yarn/index.ts @@ -14,7 +14,4 @@ * limitations under the License. */ -import { findPaths } from '@backstage/cli-common'; - -/* eslint-disable-next-line no-restricted-syntax */ -export const paths = findPaths(__dirname); +export { hasBackstageYarnPlugin } from './yarnPlugin'; diff --git a/packages/cli/src/lib/yarnPlugin.test.ts b/packages/cli-node/src/yarn/yarnPlugin.test.ts similarity index 76% rename from packages/cli/src/lib/yarnPlugin.test.ts rename to packages/cli-node/src/yarn/yarnPlugin.test.ts index d5c865e1c4..ecbff45ba7 100644 --- a/packages/cli/src/lib/yarnPlugin.test.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.test.ts @@ -15,19 +15,13 @@ */ import { createMockDirectory } from '@backstage/backend-test-utils'; -import { getHasYarnPlugin } from './yarnPlugin'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; +import { hasBackstageYarnPlugin } from './yarnPlugin'; const mockDir = createMockDirectory(); +overrideTargetPaths(mockDir.path); -jest.mock('./paths', () => ({ - paths: { - resolveTargetRoot(filename: string) { - return mockDir.resolve(filename); - }, - }, -})); - -describe('getHasYarnPlugin', () => { +describe('hasBackstageYarnPlugin', () => { beforeEach(() => { mockDir.clear(); }); @@ -35,7 +29,7 @@ describe('getHasYarnPlugin', () => { it('should return false when .yarnrc.yml does not exist', async () => { mockDir.setContent({}); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -44,7 +38,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': '', }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -53,7 +47,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': 'plugins: []', }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -66,7 +60,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -80,7 +74,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -92,7 +86,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -101,7 +95,7 @@ plugins: '.yarnrc.yml': 'invalid: yaml: content: [', }); - await expect(getHasYarnPlugin()).rejects.toThrow(); + await expect(hasBackstageYarnPlugin()).rejects.toThrow(); }); it('should throw error when .yarnrc.yml has unexpected structure', async () => { @@ -111,21 +105,22 @@ plugins: "not an array" `, }); - await expect(getHasYarnPlugin()).rejects.toThrow( + await expect(hasBackstageYarnPlugin()).rejects.toThrow( 'Unexpected content in .yarnrc.yml', ); }); - it('should handle plugins with different structure', async () => { + it('should resolve from a custom workspace directory', async () => { mockDir.setContent({ - '.yarnrc.yml': ` + 'custom-dir': { + '.yarnrc.yml': ` plugins: - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs - - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs `, + }, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(mockDir.resolve('custom-dir')); expect(result).toBe(true); }); }); diff --git a/packages/cli/src/lib/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts similarity index 74% rename from packages/cli/src/lib/yarnPlugin.ts rename to packages/cli-node/src/yarn/yarnPlugin.ts index 5ad49b2524..65383edd0c 100644 --- a/packages/cli/src/lib/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -15,9 +15,10 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; import z from 'zod'; -import { paths } from './paths'; +import { targetPaths } from '@backstage/cli-common'; const yarnRcSchema = z.object({ plugins: z @@ -30,15 +31,21 @@ const yarnRcSchema = z.object({ }); /** - * Detects whether the Backstage Yarn plugin is installed in the target repository. + * Detects whether the Backstage Yarn plugin is installed in the given workspace directory. * - * @returns Promise - true if the plugin is installed, false otherwise + * @param workspaceDir - The workspace root directory to check. Defaults to the target root. + * @returns Promise resolving to true if the plugin is installed, false otherwise + * @public */ -export async function getHasYarnPlugin(): Promise { - const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); +export async function hasBackstageYarnPlugin( + workspaceDir?: string, +): Promise { + const yarnRcPath = resolvePath( + workspaceDir ?? targetPaths.rootDir, + '.yarnrc.yml', + ); const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { if (e.code === 'ENOENT') { - // gracefully continue in case the file doesn't exist return ''; } throw e; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7f4c2a0e5b..239dceeb2a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,102 @@ # @backstage/cli +## 0.36.0-next.2 + +### Minor Changes + +- d0f4cd2: Added new `auth` command group for authenticating the CLI with Backstage instances using OAuth 2.0 with a pre-registered client metadata document. Commands include `login`, `logout`, `list`, `show`, `print-token`, and `select` for managing multiple authenticated instances. + +### Patch Changes + +- a4e5902: Internal refactor of the CLI command registration +- ff4a45a: Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. Several camelCase CLI flags have been deprecated in favor of their kebab-case equivalents (e.g. `--successCache` → `--success-cache`). The old camelCase forms still work but will now log a deprecation warning. Please update any scripts or CI configurations to use the kebab-case versions. +- 4a75544: Updated dependency `react-refresh` to `^0.18.0`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + - @backstage/integration@2.0.0-next.2 + +## 0.36.0-next.1 + +### Minor Changes + +- b36a60d: **BREAKING**: The `migrate package-exports` command has been removed. Use `repo fix` instead. + +### Patch Changes + +- 0d2d0f2: Internal refactor of CLI modularization, moving individual commands to be implemented with cleye. +- 2fcba39: Internal refactor to move shared utilities into their consuming modules, reducing cross-module dependencies. +- c85ac86: Internal refactor to split `loadCliConfig` into separate implementations for the build and config CLI modules, removing a cross-module dependency. +- 61cb976: Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. +- 825c81d: Internal refactor of CLI command modules. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## 0.35.5-next.0 + +### Patch Changes + +- 246877a: Updated dependency `bfj` to `^9.0.2`. +- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`. +- fd50cb3: Added `translations export` and `translations import` commands for managing translation files. + + The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app. + + Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping. + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 092b41f: Updated dependency `webpack` to `~5.105.0`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/module-federation-common@0.1.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## 0.35.4 + +### Patch Changes + +- cfd8103: Updated catalog provider module template to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of alpha exports. +- 20131c5: Added support for CSS exports in package builds. When a package declares a CSS file in its `exports` field (e.g., `"./styles.css": "./src/styles.css"`), the CLI will automatically bundle it during `backstage-cli package build`, resolving any `@import` statements. The export path is rewritten from `src/` to `dist/` at publish time. + + Fixed `backstage-cli repo fix` to not add `typesVersions` entries for non-script exports like CSS files. + +- 7455dae: Use node prefix on native imports +- 6ce4a13: Removed `/alpha` from `scaffolderActionsExtensionPoint` import +- fdbd404: Removed the `EXPERIMENTAL_MODULE_FEDERATION` environment variable flag, making module federation host support always available during `package start`. The host shared dependencies are now managed through `@backstage/module-federation-common` and injected as a versioned runtime script at build time. +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. +- 4fc7bf0: Bump to tar v7 +- 5e3ef57: Added support for the new `peerModules` metadata field in `package.json`. This field allows plugin packages to declare modules that should be installed alongside them for cross-plugin integrations. The field is validated by `backstage-cli repo fix --publish`. +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. +- 73351c2: Updated dependency `webpack` to `~5.104.0`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/config-loader@1.10.8 + - @backstage/eslint-plugin@0.2.1 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + - @backstage/module-federation-common@0.1.0 + ## 0.35.4-next.2 ### Patch Changes diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 0948954ff7..bb22326786 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -12,6 +12,7 @@ Options: -h, --help Commands: + auth [command] build-workspace config [command] config:check @@ -25,17 +26,97 @@ Commands: new package [command] repo [command] + translations [command] versions:bump versions:migrate ``` +### `backstage-cli auth` + +``` +Usage: backstage-cli auth [options] [command] [command] + +Options: + -h, --help + +Commands: + help [command] + list + login + logout + print-token + select + show +``` + +### `backstage-cli auth list` + +``` +Usage: backstage-cli auth list + +Options: + -h, --help +``` + +### `backstage-cli auth login` + +``` +Usage: backstage-cli auth login + +Options: + --backend-url + --instance + --no-browser + -h, --help +``` + +### `backstage-cli auth logout` + +``` +Usage: backstage-cli auth logout + +Options: + --instance + -h, --help +``` + +### `backstage-cli auth print-token` + +``` +Usage: backstage-cli auth print-token + +Options: + --instance + -h, --help +``` + +### `backstage-cli auth select` + +``` +Usage: backstage-cli auth select + +Options: + --instance + -h, --help +``` + +### `backstage-cli auth show` + +``` +Usage: backstage-cli auth show + +Options: + --instance + -h, --help +``` + ### `backstage-cli build-workspace` ``` -Usage: program [options] [packages...] +Usage: backstage-cli build-workspace [packages...] Options: - --alwaysPack + --always-pack -h, --help ``` @@ -56,87 +137,81 @@ Commands: ### `backstage-cli config docs` ``` -Usage: backstage-cli config docs [options] +Usage: backstage-cli config docs Options: - --package + --package -h, --help ``` ### `backstage-cli config schema` ``` -Usage: +Usage: backstage-cli config schema Options: - --format - --help + --format --merge - --no-merge - --package - --version + --package + -h, --help ``` ### `backstage-cli config:check` ``` -Usage: +Usage: backstage-cli config:check Options: - --config + --config --deprecated --frontend - --help --lax - --package + --package --strict - --version + -h, --help ``` ### `backstage-cli config:docs` ``` -Usage: program [options] +Usage: backstage-cli config:docs Options: - --package + --package -h, --help ``` ### `backstage-cli config:print` ``` -Usage: +Usage: backstage-cli config:print Options: - --config - --format + --config + --format --frontend - --help --lax - --package - --version + --package --with-secrets + -h, --help ``` ### `backstage-cli config:schema` ``` -Usage: +Usage: backstage-cli config:schema Options: - --format - --help + --format --merge - --no-merge - --package - --version + --package + -h, --help ``` ### `backstage-cli create-github-app` ``` -Usage: program [options] +Usage: backstage-cli create-github-app Options: -h, --help @@ -145,13 +220,12 @@ Options: ### `backstage-cli info` ``` -Usage: +Usage: backstage-cli info Options: - --format - --help - --include - --version + --format + --include + -h, --help ``` ### `backstage-cli migrate` @@ -174,7 +248,7 @@ Commands: ### `backstage-cli migrate package-exports` ``` -Usage: program [options] +Usage: backstage-cli migrate package-exports Options: -h, --help @@ -183,7 +257,7 @@ Options: ### `backstage-cli migrate package-lint-configs` ``` -Usage: program [options] +Usage: backstage-cli migrate package-lint-configs Options: -h, --help @@ -192,7 +266,7 @@ Options: ### `backstage-cli migrate package-roles` ``` -Usage: program [options] +Usage: backstage-cli migrate package-roles Options: -h, --help @@ -201,7 +275,7 @@ Options: ### `backstage-cli migrate package-scripts` ``` -Usage: program [options] +Usage: backstage-cli migrate package-scripts Options: -h, --help @@ -210,7 +284,7 @@ Options: ### `backstage-cli migrate react-router-deps` ``` -Usage: program [options] +Usage: backstage-cli migrate react-router-deps Options: -h, --help @@ -219,16 +293,16 @@ Options: ### `backstage-cli new` ``` -Usage: program [options] +Usage: backstage-cli new Options: - --baseVersion - --license - --no-private - --npm-registry - --option = - --scope - --select + --base-version + --license + --npm-registry + --option + --private + --scope + --select --skip-install -h, --help ``` @@ -255,13 +329,13 @@ Commands: ### `backstage-cli package build` ``` -Usage: program [options] +Usage: backstage-cli package build Options: - --config + --config --minify --module-federation - --role + --role --skip-build-dependencies --stats -h, --help @@ -270,7 +344,7 @@ Options: ### `backstage-cli package clean` ``` -Usage: program [options] +Usage: backstage-cli package clean Options: -h, --help @@ -279,20 +353,20 @@ Options: ### `backstage-cli package lint` ``` -Usage: program [options] [directories...] +Usage: backstage-cli package lint [directories...] Options: --fix - --format - --max-warnings - --output-file + --format + --max-warnings + --output-file -h, --help ``` ### `backstage-cli package postpack` ``` -Usage: program [options] +Usage: backstage-cli package postpack Options: -h, --help @@ -301,7 +375,7 @@ Options: ### `backstage-cli package prepack` ``` -Usage: program [options] +Usage: backstage-cli package prepack Options: -h, --help @@ -310,17 +384,17 @@ Options: ### `backstage-cli package start` ``` -Usage: program [options] +Usage: backstage-cli package start Options: --check - --config - --entrypoint - --inspect [host] - --inspect-brk [host] - --link - --require - --role + --config + --entrypoint + --inspect + --inspect-brk + --link + --require + --role -h, --help ``` @@ -459,19 +533,19 @@ Commands: ### `backstage-cli repo build` ``` -Usage: program [options] [command] +Usage: backstage-cli repo build Options: --all --minify - --since + --since -h, --help ``` ### `backstage-cli repo clean` ``` -Usage: program [options] +Usage: backstage-cli repo clean Options: -h, --help @@ -480,7 +554,7 @@ Options: ### `backstage-cli repo fix` ``` -Usage: program [options] +Usage: backstage-cli repo fix Options: --check @@ -491,23 +565,23 @@ Options: ### `backstage-cli repo lint` ``` -Usage: program [options] [command] +Usage: backstage-cli repo lint Options: --fix - --format - --max-warnings - --output-file - --since - --successCache - --successCacheDir + --format + --max-warnings + --output-file + --since + --success-cache + --success-cache-dir -h, --help ``` ### `backstage-cli repo list-deprecations` ``` -Usage: program [options] +Usage: backstage-cli repo list-deprecations Options: --json @@ -517,39 +591,75 @@ Options: ### `backstage-cli repo start` ``` -Usage: program [options] [packageNameOrPath...] +Usage: backstage-cli repo start [packages...] Options: - --config - --inspect [host] - --inspect-brk [host] - --link - --plugin - --require + --config + --inspect + --inspect-brk + --link + --plugin + --require -h, --help ``` ### `backstage-cli repo test` ``` -Usage: program [options] +Usage: backstage-cli repo test Options: --jest-help - --since - --successCache - --successCacheDir + --since + --success-cache + --success-cache-dir + -h, --help +``` + +### `backstage-cli translations` + +``` +Usage: backstage-cli translations [options] [command] [command] + +Options: + -h, --help + +Commands: + export + help [command] + import +``` + +### `backstage-cli translations export` + +``` +Usage: backstage-cli translations export + +Options: + --output + --pattern + -h, --help +``` + +### `backstage-cli translations import` + +``` +Usage: backstage-cli translations import + +Options: + --input + --output -h, --help ``` ### `backstage-cli versions:bump` ``` -Usage: program [options] +Usage: backstage-cli versions:bump Options: - --pattern - --release + --pattern + --release --skip-install --skip-migrate -h, --help @@ -558,10 +668,10 @@ Options: ### `backstage-cli versions:migrate` ``` -Usage: program [options] +Usage: backstage-cli versions:migrate Options: - --pattern + --pattern --skip-code-changes -h, --help ``` diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index dbb40537ab..4e51803802 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -269,6 +269,7 @@ function createConfigForRole(dir, role, extraConfig = {}) { }); case 'cli': + case 'cli-module': case 'node-library': case 'backend': case 'backend-plugin': diff --git a/packages/cli/config/getJestEnvironment.js b/packages/cli/config/getJestEnvironment.js index aac2f79021..e6fb66f407 100644 --- a/packages/cli/config/getJestEnvironment.js +++ b/packages/cli/config/getJestEnvironment.js @@ -14,36 +14,14 @@ * limitations under the License. */ -function getJestMajorVersion() { - const jestVersion = require('jest/package.json').version; - const majorVersion = parseInt(jestVersion.split('.')[0], 10); - return majorVersion; -} - -function getJestEnvironment() { - const majorVersion = getJestMajorVersion(); - - if (majorVersion >= 30) { - try { - require.resolve('@jest/environment-jsdom-abstract'); - require.resolve('jsdom'); - } catch { - throw new Error( - 'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' + - 'Please install them as dev dependencies.', - ); - } - return require.resolve('./jest-environment-jsdom'); - } - try { - require.resolve('jest-environment-jsdom'); - } catch { +try { + module.exports = require('@backstage/cli-module-test-jest/config/getJestEnvironment'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { throw new Error( - 'Jest 29 requires jest-environment-jsdom. ' + - 'Please install it as a dev dependency.', + '@backstage/cli-module-test-jest is required to use the jest environment configuration. ' + + 'Please install it as a dependency.', ); } - return require.resolve('jest-environment-jsdom'); + throw e; } - -module.exports = { getJestMajorVersion, getJestEnvironment }; diff --git a/packages/cli/config/jest-environment-jsdom/index.js b/packages/cli/config/jest-environment-jsdom/index.js index 9d8b76eaf5..f9be215d76 100644 --- a/packages/cli/config/jest-environment-jsdom/index.js +++ b/packages/cli/config/jest-environment-jsdom/index.js @@ -14,48 +14,14 @@ * limitations under the License. */ -const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default; -const jsdom = require('jsdom'); - -/** - * A custom JSDOM environment that extends the abstract base and applies - * fixes for Web API globals that are missing or incorrectly implemented - * in JSDOM. - * - * Based on https://github.com/mswjs/jest-fixed-jsdom - */ -class FixedJSDOMEnvironment extends JSDOMEnvironment { - constructor(config, context) { - super(config, context, jsdom); - - // Fix Web API globals that JSDOM doesn't properly expose - this.global.TextDecoder = TextDecoder; - this.global.TextEncoder = TextEncoder; - this.global.TextDecoderStream = TextDecoderStream; - this.global.TextEncoderStream = TextEncoderStream; - this.global.ReadableStream = ReadableStream; - - this.global.Blob = Blob; - this.global.Headers = Headers; - this.global.FormData = FormData; - this.global.Request = Request; - this.global.Response = Response; - this.global.fetch = fetch; - this.global.AbortController = AbortController; - this.global.AbortSignal = AbortSignal; - this.global.structuredClone = structuredClone; - this.global.URL = URL; - this.global.URLSearchParams = URLSearchParams; - - this.global.BroadcastChannel = BroadcastChannel; - this.global.TransformStream = TransformStream; - this.global.WritableStream = WritableStream; - - // Needed to ensure `e instanceof Error` works as expected with errors thrown from - // any of the native APIs above. Without this, the JSDOM `Error` is what the test - // code will use for comparison with `e`, which fails the instanceof check. - this.global.Error = Error; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jest-environment-jsdom'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest JSDOM environment. ' + + 'Please install it as a dependency.', + ); } + throw e; } - -module.exports = FixedJSDOMEnvironment; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 814f017e1d..e06d0e319e 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -14,408 +14,14 @@ * limitations under the License. */ -const fs = require('fs-extra'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const glob = require('node:util').promisify(require('glob')); -const { version } = require('../package.json'); -const paths = require('@backstage/cli-common').findPaths(process.cwd()); -const { - getJestEnvironment, - getJestMajorVersion, -} = require('./getJestEnvironment'); - -const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs']; - -const FRONTEND_ROLES = [ - 'frontend', - 'web-library', - 'common-library', - 'frontend-plugin', - 'frontend-plugin-module', -]; - -const NODE_ROLES = [ - 'backend', - 'cli', - 'node-library', - 'backend-plugin', - 'backend-plugin-module', -]; - -const envOptions = { - oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS), -}; - try { - require.resolve('react-dom/client', { - paths: [paths.targetRoot], - }); - process.env.HAS_REACT_DOM_CLIENT = true; -} catch { - /* ignored */ -} - -/** - * A list of config keys that are valid for project-level config. - * Jest will complain if we forward any other root configuration to the projects. - * - * @type {Array} - */ -const projectConfigKeys = [ - 'automock', - 'cache', - 'cacheDirectory', - 'clearMocks', - 'collectCoverageFrom', - 'coverageDirectory', - 'coveragePathIgnorePatterns', - 'cwd', - 'dependencyExtractor', - 'detectLeaks', - 'detectOpenHandles', - 'displayName', - 'errorOnDeprecated', - 'extensionsToTreatAsEsm', - 'fakeTimers', - 'filter', - 'forceCoverageMatch', - 'globalSetup', - 'globalTeardown', - 'globals', - 'haste', - 'id', - 'injectGlobals', - 'moduleDirectories', - 'moduleFileExtensions', - 'moduleNameMapper', - 'modulePathIgnorePatterns', - 'modulePaths', - 'openHandlesTimeout', - 'preset', - 'prettierPath', - 'resetMocks', - 'resetModules', - 'resolver', - 'restoreMocks', - 'rootDir', - 'roots', - 'runner', - 'runtime', - 'sandboxInjectedGlobals', - 'setupFiles', - 'setupFilesAfterEnv', - 'skipFilter', - 'skipNodeResolution', - 'slowTestThreshold', - 'snapshotResolver', - 'snapshotSerializers', - 'snapshotFormat', - 'testEnvironment', - 'testEnvironmentOptions', - 'testMatch', - 'testLocationInResults', - 'testPathIgnorePatterns', - 'testRegex', - 'testRunner', - 'transform', - 'transformIgnorePatterns', - 'watchPathIgnorePatterns', - 'unmockedModulePathPatterns', - 'workerIdleMemoryLimit', -]; - -const transformIgnorePattern = [ - '@material-ui', - 'ajv', - 'core-js', - 'jest-.*', - 'jsdom', - 'knex', - 'react', - 'react-dom', - 'highlight\\.js', - 'prismjs', - 'json-schema', - 'react-use/lib', - 'typescript', -].join('|'); - -// Provides additional config that's based on the role of the target package -function getRoleConfig(role, pkgJson) { - // Only Node.js package roles support native ESM modules, frontend and common - // packages are always transpiled to CommonJS. - const moduleOpts = NODE_ROLES.includes(role) - ? { - module: { - ignoreDynamic: true, - exportInteropAnnotation: true, - }, - } - : undefined; - - const transform = { - '\\.(mjs|cjs|js)$': [ - require.resolve('./jestSwcTransform'), - { - ...moduleOpts, - jsc: { - parser: { - syntax: 'ecmascript', - }, - }, - }, - ], - '\\.jsx$': [ - require.resolve('./jestSwcTransform'), - { - jsc: { - parser: { - syntax: 'ecmascript', - jsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - }, - }, - ], - '\\.(mts|cts|ts)$': [ - require.resolve('./jestSwcTransform'), - { - ...moduleOpts, - jsc: { - parser: { - syntax: 'typescript', - }, - }, - }, - ], - '\\.tsx$': [ - require.resolve('./jestSwcTransform'), - { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - }, - }, - ], - '\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$': - require.resolve('./jestFileTransform.js'), - '\\.(yaml)$': require.resolve('./jestYamlTransform'), - }; - if (FRONTEND_ROLES.includes(role)) { - return { - testEnvironment: getJestEnvironment(), - // The caching module loader is only used to speed up frontend tests, - // as it breaks real dynamic imports of ESM modules. - runtime: envOptions.oldTests - ? undefined - : require.resolve('./jestCachingModuleLoader'), - transform, - }; - } - return { - testEnvironment: require.resolve('jest-environment-node'), - moduleFileExtensions: [...SRC_EXTS, 'json', 'node'], - // Jest doesn't let us dynamically detect type=module per transformed file, - // so we have to assume that if the entry point is ESM, all TS files are - // ESM. - // - // This means you can't switch a package to type=module until all of its - // monorepo dependencies are also type=module or does not contain any .ts - // files. - extensionsToTreatAsEsm: - pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'], - transform, - }; -} - -async function getProjectConfig(targetPath, extraConfig, extraOptions) { - const configJsPath = path.resolve(targetPath, 'jest.config.js'); - const configTsPath = path.resolve(targetPath, 'jest.config.ts'); - // If the package has it's own jest config, we use that instead. - if (await fs.pathExists(configJsPath)) { - return require(configJsPath); - } else if (await fs.pathExists(configTsPath)) { - return require(configTsPath); - } - - // Jest config can be defined both in the root package.json and within each package. The root config - // gets forwarded to us through the `extraConfig` parameter, while the package config is read here. - // If they happen to be the same the keys will simply override each other. - // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. - const pkgJson = await fs.readJson(path.resolve(targetPath, 'package.json')); - - const options = { - ...extraConfig, - rootDir: path.resolve(targetPath, 'src'), - moduleNameMapper: { - '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), - }, - - // A bit more opinionated - testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`], - - transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], - ...getRoleConfig(pkgJson.backstage?.role, pkgJson), - }; - - options.setupFilesAfterEnv = options.setupFilesAfterEnv || []; - - if ( - extraOptions.rejectFrontendNetworkRequests && - FRONTEND_ROLES.includes(pkgJson.backstage?.role) - ) { - // By adding this first we ensure that it's possible to for example override - // fetch with a mock in a custom setup file - options.setupFilesAfterEnv.unshift( - require.resolve('./jestRejectNetworkRequests.js'), + module.exports = require('@backstage/cli-module-test-jest/config/jest'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use this jest configuration. ' + + 'Please install it as a dependency.', ); } - - if ( - options.testEnvironment === getJestEnvironment() && - getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+ - ) { - // FIXME https://github.com/jsdom/jsdom/issues/1724 - options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill')); - } - - // Use src/setupTests.* as the default location for configuring test env - for (const ext of SRC_EXTS) { - if (fs.existsSync(path.resolve(targetPath, `src/setupTests.${ext}`))) { - options.setupFilesAfterEnv.push(`/setupTests.${ext}`); - break; - } - } - - const config = Object.assign(options, pkgJson.jest); - - // The config id is a cache key that lets us share the jest cache across projects. - // If no explicit id was configured, generated one based on the configuration. - if (!config.id) { - const configHash = crypto - .createHash('sha256') - .update(version) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config.transform).replaceAll(paths.targetRoot, '')) - .digest('hex'); - config.id = `backstage_cli_${configHash}`; - } - - return config; + throw e; } - -// This loads the root jest config, which in turn will either refer to a single -// configuration for the current package, or a collection of configurations for -// the target workspace packages -async function getRootConfig() { - const rootPkgJson = await fs.readJson( - paths.resolveTargetRoot('package.json'), - ); - - const baseCoverageConfig = { - coverageDirectory: paths.resolveTarget('coverage'), - coverageProvider: envOptions.oldTests ? 'v8' : 'babel', - collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], - }; - - const { rejectFrontendNetworkRequests, ...rootOptions } = - rootPkgJson.jest ?? {}; - const extraRootOptions = { - rejectFrontendNetworkRequests, - }; - - const workspacePatterns = - rootPkgJson.workspaces && rootPkgJson.workspaces.packages; - - // Check if we're running within a specific monorepo package. In that case just get the single project config. - if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { - return getProjectConfig( - paths.targetDir, - { - ...baseCoverageConfig, - ...rootOptions, - }, - extraRootOptions, - ); - } - - const globalRootConfig = { ...baseCoverageConfig }; - const globalProjectConfig = {}; - - for (const [key, value] of Object.entries(rootOptions)) { - if (projectConfigKeys.includes(key)) { - globalProjectConfig[key] = value; - } else { - globalRootConfig[key] = value; - } - } - - // If the target package is a workspace root, we find all packages in the - // workspace and load those in as separate jest projects instead. - const projectPaths = await Promise.all( - workspacePatterns.map(pattern => - glob(path.join(paths.targetRoot, pattern)), - ), - ).then(_ => _.flat()); - - let projects = await Promise.all( - projectPaths.flat().map(async projectPath => { - const packagePath = path.resolve(projectPath, 'package.json'); - if (!(await fs.pathExists(packagePath))) { - return undefined; - } - - // We check for the presence of "backstage-cli test" in the package test - // script to determine whether a given package should be tested - const packageData = await fs.readJson(packagePath); - const testScript = packageData.scripts && packageData.scripts.test; - const isSupportedTestScript = - testScript?.includes('backstage-cli test') || - testScript?.includes('backstage-cli package test'); - if (testScript && isSupportedTestScript) { - return await getProjectConfig( - projectPath, - { - ...globalProjectConfig, - displayName: packageData.name, - }, - extraRootOptions, - ); - } - - return undefined; - }), - ).then(cs => cs.filter(Boolean)); - - const cache = global.__backstageCli_jestSuccessCache; - if (cache) { - projects = await cache.filterConfigs(projects, globalRootConfig); - } - const watchProjectFilter = global.__backstageCli_watchProjectFilter; - if (watchProjectFilter) { - projects = await watchProjectFilter.filter(projects); - } - - return { - rootDir: paths.targetRoot, - projects, - testResultsProcessor: cache - ? require.resolve('./jestCacheResultProcessor.cjs') - : undefined, - ...globalRootConfig, - }; -} - -module.exports = getRootConfig(); diff --git a/packages/cli/config/jestCacheResultProcessor.cjs b/packages/cli/config/jestCacheResultProcessor.cjs index 4af401d8b5..58217428cc 100644 --- a/packages/cli/config/jestCacheResultProcessor.cjs +++ b/packages/cli/config/jestCacheResultProcessor.cjs @@ -14,10 +14,14 @@ * limitations under the License. */ -module.exports = async results => { - const cache = global.__backstageCli_jestSuccessCache; - if (cache) { - await cache.reportResults(results); +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestCacheResultProcessor.cjs'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest cache result processor. ' + + 'Please install it as a dependency.', + ); } - return results; -}; + throw e; +} diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js index 5652ff67e7..2b6da11007 100644 --- a/packages/cli/config/jestCachingModuleLoader.js +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 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. @@ -14,22 +14,14 @@ * limitations under the License. */ -// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version -// eslint-disable-next-line @backstage/no-undeclared-imports -const { default: JestRuntime } = require('jest-runtime'); - -module.exports = class CachingJestRuntime extends JestRuntime { - constructor(config, ...restArgs) { - super(config, ...restArgs); - this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestCachingModuleLoader'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest caching module loader. ' + + 'Please install it as a dependency.', + ); } - - // Unfortunately we need to use this unstable API to make sure that .js files - // are only loaded as modules where ESM is supported, i.e. Node.js packages. - unstable_shouldLoadAsEsm(path, ...restArgs) { - if (!this.allowLoadAsEsm) { - return false; - } - return super.unstable_shouldLoadAsEsm(path, ...restArgs); - } -}; + throw e; +} diff --git a/packages/cli/config/jestFileTransform.js b/packages/cli/config/jestFileTransform.js index ad1c37a4f5..00e1c22815 100644 --- a/packages/cli/config/jestFileTransform.js +++ b/packages/cli/config/jestFileTransform.js @@ -14,31 +14,14 @@ * limitations under the License. */ -const path = require('node:path'); - -module.exports = { - process(src, filename) { - const assetFilename = JSON.stringify(path.basename(filename)); - - if (filename.match(/\.icon\.svg$/)) { - return { - code: `const React = require('react'); - const SvgIcon = require('@material-ui/core/SvgIcon').default; - module.exports = { - __esModule: true, - default: props => React.createElement(SvgIcon, props, { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }) - };`, - }; - } - - return { code: `module.exports = ${assetFilename};` }; - }, -}; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestFileTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest file transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; +} diff --git a/packages/cli/config/jestRejectNetworkRequests.js b/packages/cli/config/jestRejectNetworkRequests.js index f6f189a60c..c975257b8c 100644 --- a/packages/cli/config/jestRejectNetworkRequests.js +++ b/packages/cli/config/jestRejectNetworkRequests.js @@ -14,57 +14,14 @@ * limitations under the License. */ -const http = require('node:http'); -const https = require('node:https'); - -const errorMessage = 'Network requests are not allowed in tests'; - -const origHttpAgent = http.globalAgent; -const origHttpsAgent = https.globalAgent; -const origFetch = global.fetch; -const origXMLHttpRequest = global.XMLHttpRequest; - -http.globalAgent = new http.Agent({ - lookup() { - throw new Error(errorMessage); - }, -}); - -https.globalAgent = new https.Agent({ - lookup() { - throw new Error(errorMessage); - }, -}); - -const BLOCKING_FETCH_SYMBOL = Symbol.for( - 'backstage.jestRejectNetworkRequests.blockingFetch', -); - -if (global.fetch) { - const blockingFetch = async (input, init) => { - // If global.fetch still has our marker, block the request - if (global.fetch[BLOCKING_FETCH_SYMBOL]) { - throw new Error(errorMessage); - } - // MSW (or something else) wrapped us - pass through - return origFetch(input, init); - }; - blockingFetch[BLOCKING_FETCH_SYMBOL] = true; - global.fetch = blockingFetch; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestRejectNetworkRequests'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest network request rejection. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -if (global.XMLHttpRequest) { - global.XMLHttpRequest = class { - constructor() { - throw new Error(errorMessage); - } - }; -} - -// Reset overrides after each suite to make sure we don't pollute the test environment -afterAll(() => { - http.globalAgent = origHttpAgent; - https.globalAgent = origHttpsAgent; - global.fetch = origFetch; - global.XMLHttpRequest = origXMLHttpRequest; -}); diff --git a/packages/cli/config/jestSucraseTransform.js b/packages/cli/config/jestSucraseTransform.js index 621b9ee297..fe20351c29 100644 --- a/packages/cli/config/jestSucraseTransform.js +++ b/packages/cli/config/jestSucraseTransform.js @@ -14,74 +14,14 @@ * limitations under the License. */ -const { createHash } = require('node:crypto'); -const { transform } = require('sucrase'); -const sucrasePkg = require('sucrase/package.json'); - -const ESM_REGEX = /\b(?:import|export)\b/; - -function createTransformer(config) { - const process = (source, filePath) => { - let transforms; - - if (filePath.endsWith('.esm.js')) { - transforms = ['imports']; - } else if (filePath.endsWith('.js')) { - // This is a very rough filter to avoid transforming things that we quickly - // can be sure are definitely not ESM modules. - if (ESM_REGEX.test(source)) { - transforms = ['imports', 'jsx']; // JSX within .js is currently allowed - } - } else if (filePath.endsWith('.jsx')) { - transforms = ['jsx', 'imports']; - } else if (filePath.endsWith('.ts')) { - transforms = ['typescript', 'imports']; - } else if (filePath.endsWith('.tsx')) { - transforms = ['typescript', 'jsx', 'imports']; - } - - // Only apply the jest transform to the test files themselves - if (transforms && filePath.includes('.test.')) { - transforms.push('jest'); - } - - if (transforms) { - const { code, sourceMap: map } = transform(source, { - transforms, - filePath, - disableESTransforms: true, - sourceMapOptions: { - compiledFilename: filePath, - }, - }); - if (config.enableSourceMaps) { - const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64'); - const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`; - // Include both inline and object source maps, as inline source maps are - // needed for support of some editor integrations. - return { code: `${code}\n${suffix}`, map }; - } - // We only return the `map` result if source maps are enabled, as they - // have a negative impact on the coverage accuracy. - return { code }; - } - - return { code: source }; - }; - - const getCacheKey = sourceText => { - return createHash('sha256') - .update(sourceText) - .update(Buffer.alloc(1)) - .update(sucrasePkg.version) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config)) - .update(Buffer.alloc(1)) - .update('1') // increment whenever the transform logic in this file changes - .digest('hex'); - }; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestSucraseTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest Sucrase transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/jestSwcTransform.js b/packages/cli/config/jestSwcTransform.js index 4183349554..4aa6776c22 100644 --- a/packages/cli/config/jestSwcTransform.js +++ b/packages/cli/config/jestSwcTransform.js @@ -13,32 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const { createTransformer: createSwcTransformer } = require('@swc/jest'); -const ESM_REGEX = /\b(?:import|export)\b/; - -function createTransformer(config) { - const swcTransformer = createSwcTransformer({ - inputSourceMap: false, - ...config, - }); - const process = (source, filePath, jestOptions) => { - // Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM - if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) { - return { code: source }; - } - - // Skip transformation of .mjs files, they should only be used if ESM support is available - if (jestOptions.supportsStaticESM && filePath.endsWith('.mjs')) { - return { code: source }; - } - - return swcTransformer.process(source, filePath, jestOptions); - }; - - const getCacheKey = swcTransformer.getCacheKey; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestSwcTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest SWC transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/jestYamlTransform.js b/packages/cli/config/jestYamlTransform.js index 7a05274add..85b889498f 100644 --- a/packages/cli/config/jestYamlTransform.js +++ b/packages/cli/config/jestYamlTransform.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2020 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. @@ -14,27 +14,14 @@ * limitations under the License. */ -const yaml = require('yaml'); -const crypto = require('node:crypto'); - -function createTransformer(config) { - const process = source => { - const json = JSON.stringify(yaml.parse(source), null, 2); - return { code: `module.exports = ${json}`, map: null }; - }; - - const getCacheKey = sourceText => { - return crypto - .createHash('sha256') - .update(sourceText) - .update(Buffer.alloc(1)) - .update(JSON.stringify(config)) - .update(Buffer.alloc(1)) - .update('1') // increment whenever the transform logic in this file changes - .digest('hex'); - }; - - return { process, getCacheKey }; +try { + module.exports = require('@backstage/cli-module-test-jest/config/jestYamlTransform'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-test-jest is required to use the jest YAML transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -module.exports = { createTransformer }; diff --git a/packages/cli/config/nodeTransform.cjs b/packages/cli/config/nodeTransform.cjs index 15bcdba9bc..c6866b2964 100644 --- a/packages/cli/config/nodeTransform.cjs +++ b/packages/cli/config/nodeTransform.cjs @@ -14,74 +14,14 @@ * limitations under the License. */ -const { pathToFileURL } = require('node:url'); -const { transformSync } = require('@swc/core'); -const { addHook } = require('pirates'); -const { Module } = require('node:module'); - -// This hooks into module resolution and overrides imports of packages that -// exist in the linked workspace to instead be resolved from the linked workspace. -if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) { - const { join: joinPath } = require('node:path'); - const { getPackagesSync } = require('@manypkg/get-packages'); - const { packages: linkedPackages, root: linkedRoot } = getPackagesSync( - process.env.BACKSTAGE_CLI_LINKED_WORKSPACE, - ); - - // Matches all packages in the linked workspaces, as well as sub-path exports from them - const replacementRegex = new RegExp( - `^(?:${linkedPackages - .map(pkg => pkg.packageJson.name) - .join('|')})(?:/.*)?$`, - ); - - const origLoad = Module._load; - Module._load = function requireHook(request, parent) { - if (!replacementRegex.test(request)) { - return origLoad.call(this, request, parent); - } - - // The package import that we're overriding will always existing in the root - // node_modules of the linked workspace, so it's enough to override the - // parent paths with that single entry - return origLoad.call(this, request, { - ...parent, - paths: [joinPath(linkedRoot.dir, 'node_modules')], - }); - }; +try { + require('@backstage/cli-node/config/nodeTransform.cjs'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-node is required to use the node transform. ' + + 'Please install it as a dependency.', + ); + } + throw e; } - -addHook( - (code, filename) => { - const transformed = transformSync(code, { - filename, - sourceMaps: 'inline', - module: { - type: 'commonjs', - ignoreDynamic: true, - }, - jsc: { - target: 'es2023', - parser: { - syntax: 'typescript', - }, - }, - }); - process.send?.({ type: 'watch', path: filename }); - return transformed.code; - }, - { extensions: ['.ts', '.cts'], ignoreNodeModules: true }, -); - -addHook( - (code, filename) => { - process.send?.({ type: 'watch', path: filename }); - return code; - }, - { extensions: ['.js', '.cjs'], ignoreNodeModules: true }, -); - -// Register module hooks, used by "type": "module" in package.json, .mjs and -// .mts files, as well as dynamic import(...)s, although dynamic imports will be -// handled be the CommonJS hooks in this file if what it points to is CommonJS. -Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename)); diff --git a/packages/cli/config/nodeTransformHooks.mjs b/packages/cli/config/nodeTransformHooks.mjs index 592867e57a..274e24ed39 100644 --- a/packages/cli/config/nodeTransformHooks.mjs +++ b/packages/cli/config/nodeTransformHooks.mjs @@ -14,281 +14,7 @@ * limitations under the License. */ -import { dirname, extname, resolve as resolvePath } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { transformFile } from '@swc/core'; -import { isBuiltin } from 'node:module'; -import { readFile } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; - -// @ts-check - -// No explicit file extension, no type in package.json -const DEFAULT_MODULE_FORMAT = 'commonjs'; - -// Source file extensions to look for when using bundle resolution strategy -const SRC_EXTS = ['.ts', '.js']; -const TS_EXTS = ['.ts', '.mts', '.cts']; -const moduleTypeTable = { - '.mjs': 'module', - '.mts': 'module', - '.cjs': 'commonjs', - '.cts': 'commonjs', - '.ts': undefined, - '.js': undefined, -}; - -/** @type {import('module').ResolveHook} */ -export async function resolve(specifier, context, nextResolve) { - // Built-in modules are handled by the default resolver - if (isBuiltin(specifier)) { - return nextResolve(specifier, context); - } - - const ext = extname(specifier); - - // Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below. - if (ext === '.json' && !context.importAttributes?.type) { - const jsonResult = await nextResolve(specifier, context); - return { - ...jsonResult, - format: 'commonjs', - importAttributes: { type: 'json' }, - }; - } - - // Anything else with an explicit extension is handled by the default - // resolver, except that we help determine the module type where needed. - if (ext !== '') { - return withDetectedModuleType(await nextResolve(specifier, context)); - } - - // Other external modules are handled by the default resolver, but again we - // help determine the module type where needed. - if (!specifier.startsWith('.')) { - return withDetectedModuleType(await nextResolve(specifier, context)); - } - - // The rest of this function handles the case of resolving imports that do not - // specify any extension and might point to a directory with an `index.*` - // file. We resolve those using the same logic as most JS bundlers would, with - // the addition of checking if there's an explicit module format listed in the - // closest `package.json` file. - // - // We use a bundle resolution strategy in order to keep code consistent across - // Backstage codebases that contains code both for Web and Node.js, and to - // support packages with common code that can be used in both environments. - try { - // This is expected to throw, but in the event that this module specifier is - // supported we prefer to use the default resolver. - return await nextResolve(specifier, context); - } catch (error) { - if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { - const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`; - const resolved = await resolveWithoutExt(spec, context, nextResolve); - if (resolved) { - return withDetectedModuleType(resolved); - } - } else if (error.code === 'ERR_MODULE_NOT_FOUND') { - const resolved = await resolveWithoutExt(specifier, context, nextResolve); - if (resolved) { - return withDetectedModuleType(resolved); - } - } - - // Unexpected error or no resolution found - throw error; - } -} - -/** - * Populates the `format` field in the resolved object based on the closest `package.json` file. - * - * @param {import('module').ResolveFnOutput} resolved - * @returns {Promise} - */ -async function withDetectedModuleType(resolved) { - // Already has an explicit format - if (resolved.format) { - return resolved; - } - // Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default. - if (resolved.format === null) { - return { ...resolved, format: DEFAULT_MODULE_FORMAT }; - } - - const ext = extname(resolved.url); - - const explicitFormat = moduleTypeTable[ext]; - if (explicitFormat) { - return { - ...resolved, - format: explicitFormat, - }; - } - - // Under normal circumstances .js files should reliably have a format and so - // we should only reach this point for .ts files. However, if additional - // custom loaders are being used the format may not be detected for .js files - // either. As such we don't restrict the file format at this point. - - // TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring - const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url)); - if (!packageJsonPath) { - return resolved; - } - - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); - return { - ...resolved, - format: packageJson.type ?? DEFAULT_MODULE_FORMAT, - }; -} - -/** - * Find the closest package.json file from the given path. - * - * TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable. - * @param {string} startPath - * @returns {Promise} - */ -async function findPackageJSON(startPath) { - let path = startPath; - - // Some confidence check to avoid infinite loop - for (let i = 0; i < 1000; i++) { - const packagePath = resolvePath(path, 'package.json'); - if (existsSync(packagePath)) { - return packagePath; - } - - const newPath = dirname(path); - if (newPath === path) { - return undefined; - } - path = newPath; - } - - throw new Error( - `Iteration limit reached when searching for package.json at ${startPath}`, - ); -} - -/** @type {import('module').ResolveHook} */ -async function resolveWithoutExt(specifier, context, nextResolve) { - for (const tryExt of SRC_EXTS) { - try { - const resolved = await nextResolve(specifier + tryExt, { - ...context, - format: 'commonjs', - }); - return { - ...resolved, - format: moduleTypeTable[tryExt] ?? resolved.format, - }; - } catch { - /* ignore */ - } - } - return undefined; -} - -/** @type {import('module').LoadHook} */ -export async function load(url, context, nextLoad) { - // Non-file URLs are handled by the default loader - if (!url.startsWith('file://')) { - return nextLoad(url, context); - } - - // JSON files loaded as CommonJS are handled by this custom loader, because - // the default one doesn't work. For JSON loading to work we'd need the - // synchronous hooks that aren't supported yet, or avoid using the CommonJS - // compatibility. - if ( - context.format === 'commonjs' && - context.importAttributes?.type === 'json' - ) { - try { - // TODO(Rugvip): Make sure this is valid JSON - const content = await readFile(fileURLToPath(url), 'utf8'); - return { - source: `module.exports = (${content})`, - format: 'commonjs', - shortCircuit: true, - }; - } catch { - // Let the default loader generate the error - return nextLoad(url, context); - } - } - - const ext = extname(url); - - // Non-TS files are handled by the default loader - if (!TS_EXTS.includes(ext)) { - return nextLoad(url, context); - } - - const format = context.format ?? DEFAULT_MODULE_FORMAT; - - // We have two choices at this point, we can either transform CommonJS files - // and return the transformed source code, or let the default loader handle - // them. If we transform them ourselves we will enter CommonJS compatibility - // mode in the new module system in Node.js, this effectively means all - // CommonJS loaded via `require` calls from this point will all be treated as - // if it was loaded via `import` calls from modules. - // - // The CommonJS compatibility layer will try to identify named exports and - // make them available directly, which is convenient as it avoids things like - // `import(...).then(m => m.default.foo)`, allowing you to instead write - // `import(...).then(m => m.foo)`. The compatibility layer doesn't always work - // all that well though, and can lead to module loading issues in many cases, - // especially for older code. - - // This `if` block opts-out of using CommonJS compatibility mode by default, - // and instead leaves it to our existing loader to transform CommonJS. We do - // however use compatibility mode for the more explicit .cts file extension, - // allows for a way to opt-in to the new behavior. - // - // TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead - if (format === 'commonjs' && ext !== '.cts') { - return nextLoad(url, { ...context, format }); - } - - // If the Node.js version we're running supports TypeScript, i.e. type - // stripping, we hand over to the default loader. This is done for all cases - // except if we're loading a .ts file that's been resolved to CommonJS format. - // This is because these files aren't actually CommonJS in the Backstage build - // system, and need to be transformed to CommonJS. - if ( - format === 'module-typescript' || - (format === 'module-commonjs' && ext !== '.ts') - ) { - return nextLoad(url, { ...context, format }); - } - - const transformed = await transformFile(fileURLToPath(url), { - sourceMaps: 'inline', - module: { - type: format === 'module' ? 'es6' : 'commonjs', - ignoreDynamic: true, - - // This helps the Node.js CommonJS compat layer identify named exports. - exportInteropAnnotation: true, - }, - jsc: { - target: 'es2023', - parser: { - syntax: 'typescript', - }, - }, - }); - - return { - ...context, - shortCircuit: true, - source: transformed.code, - format, - responseURL: url, - }; -} +export { + resolve, + load, +} from '@backstage/cli-node/config/nodeTransformHooks.mjs'; diff --git a/packages/cli/config/webpack-public-path.js b/packages/cli/config/webpack-public-path.js index 3e14e96eb9..d1c36222c9 100644 --- a/packages/cli/config/webpack-public-path.js +++ b/packages/cli/config/webpack-public-path.js @@ -14,18 +14,14 @@ * limitations under the License. */ -// This script is used to pick up and set the public path of the Webpack bundle -// at runtime. The meta tag is injected by the app build, but only present in -// the `index.html.tmpl` file. The runtime value of the meta tag is populated by -// the app backend, when it templates the final `index.html` file. -// -// This is needed for additional chunks to use the correct public path, and it -// is not possible to set the `__webpack_public_path__` variable outside of the -// build itself. The Webpack output also does not read any tags or -// similar, this seems to be the only way to dynamically configure the public -// path at runtime. -const el = document.querySelector('meta[name="backstage-public-path"]'); -const path = el?.getAttribute('content'); -if (path) { - __webpack_public_path__ = path; +try { + require('@backstage/cli-module-build/config/webpack-public-path'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error( + '@backstage/cli-module-build is required to use the webpack public path configuration. ' + + 'Please install it as a dependency.', + ); + } + throw e; } diff --git a/packages/cli/package.json b/packages/cli/package.json index fbe1b87253..4ba287a9ef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.35.4-next.2", + "version": "0.36.0-next.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -47,51 +47,27 @@ ] }, "dependencies": { - "@backstage/catalog-model": "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/config-loader": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/eslint-plugin": "workspace:^", - "@backstage/integration": "workspace:^", - "@backstage/module-federation-common": "workspace:^", - "@backstage/release-manifests": "workspace:^", - "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@module-federation/enhanced": "^0.21.6", - "@octokit/request": "^8.0.0", - "@rollup/plugin-commonjs": "^26.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-yaml": "^4.0.0", - "@rspack/core": "^1.4.11", - "@rspack/dev-server": "^1.1.4", - "@rspack/plugin-react-refresh": "^1.4.3", "@spotify/eslint-config-base": "^15.0.0", "@spotify/eslint-config-react": "^15.0.0", "@spotify/eslint-config-typescript": "^15.0.0", "@swc/core": "^1.15.6", - "@swc/helpers": "^0.5.17", "@swc/jest": "^0.2.39", "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "^3.0.0", - "bfj": "^8.0.0", - "buffer": "^6.0.3", "chalk": "^4.0.0", - "chokidar": "^3.3.1", - "commander": "^12.0.0", + "commander": "^14.0.3", "cross-fetch": "^4.0.0", - "cross-spawn": "^7.0.3", - "css-loader": "^6.5.1", - "ctrlc-windows": "^2.1.0", - "esbuild": "^0.27.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", - "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.9.0", @@ -99,53 +75,13 @@ "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unused-imports": "^4.1.4", - "eslint-rspack-plugin": "^4.2.1", - "express": "^4.22.0", "fs-extra": "^11.2.0", - "git-url-parse": "^15.0.0", "glob": "^7.1.7", - "global-agent": "^3.0.0", - "globby": "^11.1.0", - "handlebars": "^4.7.3", - "html-webpack-plugin": "^5.6.3", - "inquirer": "^8.2.0", "jest-css-modules": "^2.1.0", - "json-schema": "^0.4.0", - "lodash": "^4.17.21", - "minimatch": "^9.0.0", - "node-stdlib-browser": "^1.3.1", - "npm-packlist": "^5.0.0", - "ora": "^5.3.0", - "p-queue": "^6.6.2", "pirates": "^4.0.6", "postcss": "^8.1.0", - "postcss-import": "^16.1.0", - "process": "^0.11.10", - "raw-loader": "^4.0.2", - "react-dev-utils": "^12.0.0-next.60", - "react-refresh": "^0.17.0", - "recursive-readdir": "^2.2.2", - "replace-in-file": "^7.1.0", - "rollup": "^4.27.3", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-esbuild": "^6.1.1", - "rollup-plugin-postcss": "^4.0.0", - "rollup-pluginutils": "^2.8.2", - "semver": "^7.5.3", - "style-loader": "^3.3.1", "sucrase": "^3.20.2", - "swc-loader": "^0.2.3", - "tar": "^7.5.6", - "ts-checker-rspack-plugin": "^1.1.5", - "ts-morph": "^24.0.0", - "undici": "^7.2.3", - "util": "^0.12.3", - "yaml": "^2.0.0", - "yargs": "^16.2.0", - "yml-loader": "^2.1.0", - "yn": "^4.0.0", - "zod": "^3.25.76", - "zod-validation-error": "^4.0.2" + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/backend-plugin-api": "workspace:^", @@ -165,88 +101,28 @@ "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", "@jest/environment-jsdom-abstract": "^30.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", - "@types/cross-spawn": "^6.0.2", - "@types/ejs": "^3.1.3", - "@types/express": "^4.17.6", "@types/fs-extra": "^11.0.0", - "@types/http-proxy": "^1.17.4", - "@types/inquirer": "^8.1.3", "@types/jest": "^30.0.0", "@types/node": "^22.13.14", - "@types/npm-packlist": "^3.0.0", - "@types/recursive-readdir": "^2.2.0", - "@types/rollup-plugin-peer-deps-external": "^2.2.0", - "@types/rollup-plugin-postcss": "^3.1.4", - "@types/svgo": "^2.6.2", - "@types/tar": "^6.1.1", - "@types/terser-webpack-plugin": "^5.0.4", - "@types/webpack-sources": "^3.2.3", - "@types/yarnpkg__lockfile": "^1.1.4", - "del": "^8.0.0", - "esbuild-loader": "^4.0.0", - "eslint-webpack-plugin": "^4.2.0", - "fork-ts-checker-webpack-plugin": "^9.0.0", "jest": "^30.2.0", "jsdom": "^27.1.0", - "mini-css-extract-plugin": "^2.4.2", - "msw": "^1.0.0", - "nodemon": "^3.0.1", - "terser-webpack-plugin": "^5.1.3", - "webpack": "~5.104.0", - "webpack-dev-server": "^5.0.0" + "nodemon": "^3.0.1" }, "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.104.0", - "webpack-dev-server": "^5.0.0" + "jsdom": "^27.1.0" }, "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": { - "optional": true - }, - "terser-webpack-plugin": { - "optional": true - }, - "webpack": { - "optional": true - }, - "webpack-dev-server": { - "optional": true } }, "configSchema": { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ece1e413a0..483717fc64 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,18 +14,35 @@ * limitations under the License. */ +import chalk from 'chalk'; import { CliInitializer } from './wiring/CliInitializer'; +import { discoverCliModules } from './wiring/discoverCliModules'; (async () => { const initializer = new CliInitializer(); - initializer.add(import('./modules/build')); - initializer.add(import('./modules/config')); - initializer.add(import('./modules/create-github-app')); - initializer.add(import('./modules/info')); - initializer.add(import('./modules/lint')); - initializer.add(import('./modules/maintenance')); - initializer.add(import('./modules/migrate')); - initializer.add(import('./modules/new')); - initializer.add(import('./modules/test')); + + const discoveredModules = discoverCliModules(); + + if (discoveredModules.length > 0) { + for (const resolvedPath of discoveredModules) { + initializer.add(import(resolvedPath)); + } + } else { + // No CLI modules found in the project root; fall back to the built-in + // set while printing a deprecation warning. + console.error( + chalk.yellow( + `No CLI modules found in the project root dependencies. ` + + `Falling back to the built-in set of modules.\n` + + `This fallback will be removed in a future release. ` + + `Please add @backstage/cli-defaults as a devDependency ` + + `in your root package.json, or install individual ` + + `@backstage/cli-module-* packages for fine-grained control.\n`, + ), + ); + + initializer.add(import('@backstage/cli-defaults')); + } + await initializer.run(); })(); diff --git a/packages/cli/src/lib/optionsParser.ts b/packages/cli/src/lib/optionsParser.ts deleted file mode 100644 index 45c7eac0aa..0000000000 --- a/packages/cli/src/lib/optionsParser.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2024 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 { Command } from 'commander'; - -export function createScriptOptionsParser( - anyCmd: Command, - commandPath: string[], -) { - // Regardless of what command instance is passed in we want to find - // the root command and resolve the path from there - let rootCmd = anyCmd; - while (rootCmd.parent) { - rootCmd = rootCmd.parent; - } - - // Now find the command that was requested - let targetCmd = rootCmd as Command | undefined; - for (const name of commandPath) { - targetCmd = targetCmd?.commands.find(c => c.name() === name) as - | Command - | undefined; - } - - if (!targetCmd) { - throw new Error( - `Could not find package command '${commandPath.join(' ')}'`, - ); - } - const cmd = targetCmd; - - const expectedScript = `backstage-cli ${commandPath.join(' ')}`; - - return (scriptStr?: string) => { - if (!scriptStr || !scriptStr.startsWith(expectedScript)) { - return undefined; - } - - const argsStr = scriptStr.slice(expectedScript.length).trim(); - - // Can't clone or copy or even use commands as prototype, so we mutate - // the necessary members instead, and then reset them once we're done - const currentOpts = (cmd as any)._optionValues; - const currentStore = (cmd as any)._storeOptionsAsProperties; - - const result: Record = {}; - (cmd as any)._storeOptionsAsProperties = false; - (cmd as any)._optionValues = result; - - // Triggers the writing of options to the result object - cmd.parseOptions(argsStr.split(' ')); - - (cmd as any)._storeOptionsAsProperties = currentOpts; - (cmd as any)._optionValues = currentStore; - - return result; - }; -} diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts deleted file mode 100644 index 389e22102b..0000000000 --- a/packages/cli/src/lib/parallel.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2020 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 os from 'node:os'; -import { - parseParallelismOption, - getEnvironmentParallelism, - runParallelWorkers, - runWorkerQueueThreads, - runWorkerThreads, -} from './parallel'; - -const defaultParallelism = Math.ceil(os.cpus().length / 2); - -describe('parseParallelismOption', () => { - it('coerces false no parallelism', () => { - expect(parseParallelismOption(false)).toBe(1); - expect(parseParallelismOption('false')).toBe(1); - }); - - it('coerces true or undefined to default parallelism', () => { - expect(parseParallelismOption(true)).toBe(defaultParallelism); - expect(parseParallelismOption('true')).toBe(defaultParallelism); - expect(parseParallelismOption(undefined)).toBe(defaultParallelism); - expect(parseParallelismOption(null)).toBe(defaultParallelism); - }); - - it('coerces number string to number', () => { - expect(parseParallelismOption('2')).toBe(2); - }); - - it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => { - expect(() => parseParallelismOption(value as any)).toThrow( - `Parallel option value '${value}' is not a boolean or integer`, - ); - }); -}); - -describe('getEnvironmentParallelism', () => { - it('reads the parallelism setting from the environment', () => { - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2'; - expect(getEnvironmentParallelism()).toBe(2); - - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true'; - expect(getEnvironmentParallelism()).toBe(defaultParallelism); - - process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false'; - expect(getEnvironmentParallelism()).toBe(1); - - delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; - expect(getEnvironmentParallelism()).toBe(defaultParallelism); - }); -}); - -describe('runParallelWorkers', () => { - it('executes work in parallel', async () => { - const started = new Array(); - const done = new Array(); - const waiting = new Array<() => void>(); - - const work = runParallelWorkers({ - items: [0, 1, 2, 3, 4], - parallelismSetting: 4, - parallelismFactor: 0.5, // 2 at a time - worker: async item => { - started.push(item); - await new Promise(resolve => { - waiting[item] = resolve; - }); - done.push(item); - }, - }); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1]); - expect(done).toEqual([]); - waiting[0](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1, 2]); - expect(done).toEqual([0]); - waiting[1](); - waiting[2](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1, 2, 3, 4]); - expect(done).toEqual([0, 1, 2]); - waiting[3](); - waiting[4](); - - await work; - expect(done).toEqual([0, 1, 2, 3, 4]); - }); - - it('executes work sequentially', async () => { - const started = new Array(); - const done = new Array(); - const waiting = new Array<() => void>(); - - const work = runParallelWorkers({ - items: [0, 1, 2, 3, 4], - parallelismFactor: 0, // 1 at a time - worker: async item => { - started.push(item); - await new Promise(resolve => { - waiting[item] = resolve; - }); - done.push(item); - }, - }); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0]); - expect(done).toEqual([]); - waiting[0](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1]); - expect(done).toEqual([0]); - waiting[1](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1, 2]); - waiting[2](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1, 2, 3]); - waiting[3](); - - await new Promise(resolve => setTimeout(resolve)); - expect(started).toEqual([0, 1, 2, 3, 4]); - waiting[4](); - - await work; - expect(done).toEqual([0, 1, 2, 3, 4]); - }); -}); - -describe('runWorkerQueueThreads', () => { - it('should execute work in parallel', async () => { - const sharedData = new SharedArrayBuffer(10); - const sharedView = new Uint8Array(sharedData); - - const results = await runWorkerQueueThreads({ - threadCount: 4, - workerData: sharedData, - items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - workerFactory: data => { - const view = new Uint8Array(data); - - return async (i: number) => { - view[i] = 10 + i; - return 20 + i; - }; - }, - }); - - expect(Array.from(sharedView)).toEqual([ - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - ]); - expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); - }); -}); - -describe('runWorkerThreads', () => { - it('should run a single thread without items', async () => { - const [result] = await runWorkerThreads({ - threadCount: 1, - workerData: 'foo', - worker: async data => `${data}bar`, - }); - - expect(result).toBe('foobar'); - }); - - it('should run multiple threads without items', async () => { - const results = await runWorkerThreads({ - threadCount: 4, - worker: async () => 'foo', - }); - - expect(results).toEqual(['foo', 'foo', 'foo', 'foo']); - }); - - it('should send messages', async () => { - const messages = new Array(); - - await runWorkerThreads({ - threadCount: 2, - worker: async (_data, sendMessage) => { - sendMessage('a'); - await new Promise(resolve => setTimeout(resolve, 10)); - sendMessage('b'); - await new Promise(resolve => setTimeout(resolve, 10)); - sendMessage('c'); - }, - onMessage: (message: string) => messages.push(message), - }); - - expect(messages.sort()).toEqual(['a', 'a', 'b', 'b', 'c', 'c']); - }); -}); diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli/src/lib/parallel.ts deleted file mode 100644 index a4ca5dd13d..0000000000 --- a/packages/cli/src/lib/parallel.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright 2020 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 os from 'node:os'; -import { ErrorLike } from '@backstage/errors'; -import { Worker } from 'node:worker_threads'; - -const defaultParallelism = Math.ceil(os.cpus().length / 2); - -const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; - -export type ParallelismOption = boolean | string | number | null | undefined; - -export function parseParallelismOption(parallel: ParallelismOption): number { - if (parallel === undefined || parallel === null) { - return defaultParallelism; - } else if (typeof parallel === 'boolean') { - return parallel ? defaultParallelism : 1; - } else if (typeof parallel === 'number' && Number.isInteger(parallel)) { - if (parallel < 1) { - return 1; - } - return parallel; - } else if (typeof parallel === 'string') { - if (parallel === 'true') { - return parseParallelismOption(true); - } else if (parallel === 'false') { - return parseParallelismOption(false); - } - const parsed = Number(parallel); - if (Number.isInteger(parsed)) { - return parseParallelismOption(parsed); - } - } - - throw Error( - `Parallel option value '${parallel}' is not a boolean or integer`, - ); -} - -export function getEnvironmentParallelism() { - return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); -} - -type ParallelWorkerOptions = { - /** - * Decides the number of parallel workers by multiplying - * this with the configured parallelism, which defaults to 4. - * - * Defaults to 1. - */ - parallelismFactor?: number; - parallelismSetting?: ParallelismOption; - items: Iterable; - worker: (item: TItem) => Promise; -}; - -export async function runParallelWorkers( - options: ParallelWorkerOptions, -) { - const { parallelismFactor = 1, parallelismSetting, items, worker } = options; - const parallelism = parallelismSetting - ? parseParallelismOption(parallelismSetting) - : getEnvironmentParallelism(); - - const sharedIterator = items[Symbol.iterator](); - const sharedIterable = { - [Symbol.iterator]: () => sharedIterator, - }; - - const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1); - return Promise.all( - Array(workerCount) - .fill(0) - .map(async () => { - for (const value of sharedIterable) { - await worker(value); - } - }), - ); -} - -type WorkerThreadMessage = - | { - type: 'done'; - } - | { - type: 'item'; - index: number; - item: unknown; - } - | { - type: 'start'; - } - | { - type: 'result'; - index: number; - result: unknown; - } - | { - type: 'error'; - error: ErrorLike; - } - | { - type: 'message'; - message: unknown; - }; - -export type WorkerQueueThreadsOptions = { - /** The items to process */ - items: Iterable; - /** - * A function that will be called within each worker thread at startup, - * which should return the worker function that will be called for each item. - * - * This function must be defined as an arrow function or using the - * function keyword, and must be entirely self contained, not referencing - * any variables outside of its scope. This is because the function source - * is stringified and evaluated in the worker thread. - * - * To pass data to the worker, use the `workerData` option and `items`, but - * note that they are both copied by value into the worker thread, except for - * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. - */ - workerFactory: ( - data: TData, - ) => - | ((item: TItem) => Promise) - | Promise<(item: TItem) => Promise>; - /** Data supplied to each worker factory */ - workerData?: TData; - /** Number of threads, defaults to half of the number of available CPUs */ - threadCount?: number; -}; - -/** - * Spawns one or more worker threads using the `worker_threads` module. - * Each thread processes one item at a time from the provided `options.items`. - */ -export async function runWorkerQueueThreads( - options: WorkerQueueThreadsOptions, -): Promise { - const items = Array.from(options.items); - const { - workerFactory, - workerData, - threadCount = Math.min(getEnvironmentParallelism(), items.length), - } = options; - - const iterator = items[Symbol.iterator](); - const results = new Array(); - let itemIndex = 0; - - await Promise.all( - Array(threadCount) - .fill(0) - .map(async () => { - const thread = new Worker(`(${workerQueueThread})(${workerFactory})`, { - eval: true, - workerData, - }); - - return new Promise((resolve, reject) => { - thread.on('message', (message: WorkerThreadMessage) => { - if (message.type === 'start' || message.type === 'result') { - if (message.type === 'result') { - results[message.index] = message.result as TResult; - } - const { value, done } = iterator.next(); - if (done) { - thread.postMessage({ type: 'done' }); - } else { - thread.postMessage({ - type: 'item', - index: itemIndex, - item: value, - }); - itemIndex += 1; - } - } else if (message.type === 'error') { - const error = new Error(message.error.message); - error.name = message.error.name; - error.stack = message.error.stack; - reject(error); - } - }); - - thread.on('error', reject); - thread.on('exit', (code: number) => { - if (code !== 0) { - reject(new Error(`Worker thread exited with code ${code}`)); - } else { - resolve(); - } - }); - }); - }), - ); - - return results; -} - -/* istanbul ignore next */ -function workerQueueThread( - workerFuncFactory: ( - data: unknown, - ) => Promise<(item: unknown) => Promise>, -) { - const { parentPort, workerData } = require('node:worker_threads'); - - Promise.resolve() - .then(() => workerFuncFactory(workerData)) - .then( - workerFunc => { - parentPort.on('message', async (message: WorkerThreadMessage) => { - if (message.type === 'done') { - parentPort.close(); - return; - } - if (message.type === 'item') { - try { - const result = await workerFunc(message.item); - parentPort.postMessage({ - type: 'result', - index: message.index, - result, - }); - } catch (error) { - parentPort.postMessage({ type: 'error', error }); - } - } - }); - - parentPort.postMessage({ type: 'start' }); - }, - error => parentPort.postMessage({ type: 'error', error }), - ); -} - -export type WorkerThreadsOptions = { - /** - * A function that is called by each worker thread to produce a result. - * - * This function must be defined as an arrow function or using the - * function keyword, and must be entirely self contained, not referencing - * any variables outside of its scope. This is because the function source - * is stringified and evaluated in the worker thread. - * - * To pass data to the worker, use the `workerData` option, but - * note that they are both copied by value into the worker thread, except for - * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. - */ - worker: ( - data: TData, - sendMessage: (message: TMessage) => void, - ) => Promise; - /** Data supplied to each worker */ - workerData?: TData; - /** Number of threads, defaults to 1 */ - threadCount?: number; - /** An optional handler for messages posted from the worker thread */ - onMessage?: (message: TMessage) => void; -}; - -/** - * Spawns one or more worker threads using the `worker_threads` module. - */ -export async function runWorkerThreads( - options: WorkerThreadsOptions, -): Promise { - const { worker, workerData, threadCount = 1, onMessage } = options; - - return Promise.all( - Array(threadCount) - .fill(0) - .map(async () => { - const thread = new Worker(`(${workerThread})(${worker})`, { - eval: true, - workerData, - }); - - return new Promise((resolve, reject) => { - thread.on('message', (message: WorkerThreadMessage) => { - if (message.type === 'result') { - resolve(message.result as TResult); - } else if (message.type === 'error') { - reject(message.error); - } else if (message.type === 'message') { - onMessage?.(message.message as TMessage); - } - }); - - thread.on('error', reject); - thread.on('exit', (code: number) => { - reject( - new Error(`Unexpected worker thread exit with code ${code}`), - ); - }); - }); - }), - ); -} - -/* istanbul ignore next */ -function workerThread( - workerFunc: ( - data: unknown, - sendMessage: (message: unknown) => void, - ) => Promise, -) { - const { parentPort, workerData } = require('node:worker_threads'); - - const sendMessage = (message: unknown) => { - parentPort.postMessage({ type: 'message', message }); - }; - - workerFunc(workerData, sendMessage).then( - result => { - parentPort.postMessage({ - type: 'result', - index: 0, - result, - }); - }, - error => { - parentPort.postMessage({ type: 'error', error }); - }, - ); -} diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts deleted file mode 100644 index d6996b9143..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 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 { Lockfile } from './Lockfile'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - -`; - -const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 -`; - -const mockA = `${LEGACY_HEADER} -a@^1: - version "1.0.1" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz - dependencies: - b "^2" - -b@2.0.x: - version "2.0.1" - -b@^2: - version "2.0.0" -`; - -describe('Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockA', async () => { - mockDir.setContent({ - 'yarn.lock': mockA, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); - expect(lockfile.get('a')).toEqual([ - { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, - ]); - expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, - { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, - ]); - expect(lockfile.toString()).toBe(mockA); - }); -}); - -const mockANew = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 2.0.1 - -b@^2: - version: 2.0.0 -`; - -describe('New Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockANew', async () => { - mockDir.setContent({ - 'yarn.lock': mockANew, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); - expect(lockfile.get('a')).toEqual([ - { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, - ]); - expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, - { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, - { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, - ]); - expect(lockfile.toString()).toBe(mockANew); - }); -}); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts deleted file mode 100644 index 7a9a2c801a..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2020 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 { parseSyml, stringifySyml } from '@yarnpkg/parsers'; -import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; - -const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; - -type LockfileData = { - [entry: string]: { - version: string; - resolved?: string; - integrity?: string /* old */; - checksum?: string /* new */; - dependencies?: { [name: string]: string }; - peerDependencies?: { [name: string]: string }; - }; -}; - -type LockfileQueryEntry = { - range: string; - version: string; - dataKey: string; -}; - -// the new yarn header is handled out of band of the parsing -// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 -const NEW_HEADER = `${[ - `# This file is generated by running "yarn install" inside your project.\n`, - `# Manual changes might be lost - proceed with caution!\n`, -].join(``)}\n`; - -// taken from yarn parser package -// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 -const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; - -// these are special top level yarn keys. -// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9 -const SPECIAL_OBJECT_KEYS = [ - `__metadata`, - `version`, - `resolution`, - `dependencies`, - `peerDependencies`, - `dependenciesMeta`, - `peerDependenciesMeta`, - `binaries`, -]; - -export class Lockfile { - static async load(path: string) { - const lockfileContents = await fs.readFile(path, 'utf8'); - return Lockfile.parse(lockfileContents); - } - - static parse(content: string) { - const legacy = LEGACY_REGEX.test(content); - - let data: LockfileData; - try { - data = parseSyml(content); - } catch (err) { - throw new Error(`Failed yarn.lock parse, ${err}`); - } - - const packages = new Map(); - - for (const [key, value] of Object.entries(data)) { - if (SPECIAL_OBJECT_KEYS.includes(key)) continue; - - const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? []; - if (!name) { - throw new Error(`Failed to parse yarn.lock entry '${key}'`); - } - - let queries = packages.get(name); - if (!queries) { - queries = []; - packages.set(name, queries); - } - for (let range of ranges.split(/\s*,\s*/)) { - if (range.startsWith(`${name}@`)) { - range = range.slice(`${name}@`.length); - } - if (range.startsWith('npm:')) { - range = range.slice('npm:'.length); - } - queries.push({ range, version: value.version, dataKey: key }); - } - } - - return new Lockfile(packages, data, legacy); - } - - private readonly packages: Map; - private readonly data: LockfileData; - private readonly legacy: boolean; - - private constructor( - packages: Map, - data: LockfileData, - legacy: boolean = false, - ) { - this.packages = packages; - this.data = data; - this.legacy = legacy; - } - - /** Get the entries for a single package in the lockfile */ - get(name: string): LockfileQueryEntry[] | undefined { - return this.packages.get(name); - } - - /** Returns the name of all packages available in the lockfile */ - keys(): IterableIterator { - return this.packages.keys(); - } - - toString() { - return this.legacy - ? legacyStringifyLockfile(this.data) - : NEW_HEADER + stringifySyml(this.data); - } -} diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts deleted file mode 100644 index b38957f488..0000000000 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 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 { OptionValues } from 'commander'; -import { startPackage } from './startPackage'; -import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; -import { findRoleFromCommand } from '../../../../../lib/role'; -import { paths } from '../../../../../lib/paths'; - -export async function command(opts: OptionValues): Promise { - await startPackage({ - role: await findRoleFromCommand(opts), - entrypoint: opts.entrypoint, - targetDir: paths.targetDir, - configPaths: opts.config as string[], - checksEnabled: Boolean(opts.check), - linkedWorkspace: await resolveLinkedWorkspace(opts.link), - inspectEnabled: opts.inspect, - inspectBrkEnabled: opts.inspectBrk, - require: opts.require, - }); -} diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts deleted file mode 100644 index 9b37efaa8b..0000000000 --- a/packages/cli/src/modules/build/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2024 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 { Command, Option } from 'commander'; -import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; -import { configOption } from '../config'; - -export function registerPackageCommands(command: Command) { - command - .command('build') - .description('Build a package for production deployment or publishing') - .option('--role ', 'Run the command with an explicit package role') - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies. Applies to backend packages only.', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory. Applies to app packages only.', - ) - .option( - '--config ', - 'Config files to load instead of app-config.yaml. Applies to app packages only.', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option( - '--module-federation', - 'Build a package as a module federation remote. Applies to frontend plugin packages only.', - ) - .action(lazy(() => import('./commands/package/build'), 'command')); -} - -export const buildPlugin = createCliPlugin({ - pluginId: 'build', - init: async reg => { - reg.addCommand({ - path: ['package', 'build'], - description: 'Build a package for production deployment or publishing', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option( - '--role ', - 'Run the command with an explicit package role', - ) - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies. Applies to backend packages only.', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory. Applies to app packages only.', - ) - .option( - '--config ', - 'Config files to load instead of app-config.yaml. Applies to app packages only.', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option( - '--module-federation', - 'Build a package as a module federation remote. Applies to frontend plugin packages only.', - ) - .action(lazy(() => import('./commands/package/build'), 'command')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'build'], - description: - 'Build packages in the project, excluding bundled app and backend packages.', - execute: async ({ args }) => { - const command = new Command(); - - // This command expect `package build` to be registered, as its used to parse - // individual plugins' package build scripts. - registerPackageCommands(command.command('package')); - - const defaultCommand = command - .option( - '--all', - 'Build all packages, including bundled app and backend packages.', - ) - .option( - '--since ', - 'Only build packages and their dev dependents that changed since the specified ref', - ) - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .action(lazy(() => import('./commands/repo/build'), 'command')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'start'], - description: 'Start a package for local development', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option(...configOption) - .option( - '--role ', - 'Run the command with an explicit package role', - ) - .option('--check', 'Enable type checking and linting if available') - .option('--inspect [host]', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .option( - '--require ', - 'Add a --require argument to the node process', - ) - .option( - '--link ', - 'Link an external workspace for module resolution', - ) - .option( - '--entrypoint ', - 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', - ) - .action(lazy(() => import('./commands/package/start'), 'command')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'start'], - description: 'Starts packages in the repo for local development', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .argument( - '[packageNameOrPath...]', - 'Run the specified package instead of the defaults.', - ) - .option( - '--plugin ', - 'Start the dev entry-point for any matching plugin package in the repo', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option(...configOption) - .option( - '--inspect [host]', - 'Enable debugger in Node.js environments. Applies to backend package only', - ) - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', - ) - .option( - '--require ', - 'Add a --require argument to the node process. Applies to backend package only', - ) - .option( - '--link ', - 'Link an external workspace for module resolution', - ) - .action( - lazy(() => import('../build/commands/repo/start'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['build-workspace'], - description: - 'Builds a temporary dist workspace from the provided packages', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .arguments(' [packages...]') - .addOption( - new Option( - '--alwaysYarnPack', - 'Alias for --alwaysPack for backwards compatibility.', - ) - .implies({ alwaysPack: true }) - .hideHelp(true), - ) - .option( - '--alwaysPack', - 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', - ) - .action(lazy(() => import('./commands/buildWorkspace'), 'default')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); - -export default buildPlugin; diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts deleted file mode 100644 index 3eabf5e9fc..0000000000 --- a/packages/cli/src/modules/config/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2024 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 { createCliPlugin } from '../../wiring/factory'; -import yargs from 'yargs'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; - -export const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), -] as const; - -export default createCliPlugin({ - pluginId: 'config', - init: async reg => { - reg.addCommand({ - path: ['config:docs'], - description: 'Browse the configuration reference documentation', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--package ', - 'Only include the schema that applies to the given package', - ) - .description('Browse the configuration reference documentation') - .action(lazy(() => import('./commands/docs'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - reg.addCommand({ - path: ['config', 'docs'], - description: 'Browse the configuration reference documentation', - execute: async ({ args, info }) => { - await new Command(info.usage) - .option( - '--package ', - 'Only include the schema that applies to the given package', - ) - .description(info.description) - .action(lazy(() => import('./commands/docs'), 'default')) - .parseAsync(args, { from: 'user' }); - }, - }); - reg.addCommand({ - path: ['config:print'], - description: 'Print the app configuration for the current package', - execute: async ({ args, info }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - lax: { type: 'boolean' }, - frontend: { type: 'boolean' }, - 'with-secrets': { type: 'boolean' }, - format: { type: 'string' }, - config: { type: 'string', array: true, default: [] }, - }) - .usage('$0', info.description) - .help() - .parse(args); - await lazy(() => import('./commands/print'), 'default')(argv); - }, - }); - reg.addCommand({ - path: ['config:check'], - description: - 'Validate that the given configuration loads and matches schema', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - lax: { type: 'boolean' }, - frontend: { type: 'boolean' }, - deprecated: { type: 'boolean' }, - strict: { type: 'boolean' }, - config: { - type: 'string', - array: true, - default: [], - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/validate'), 'default')(argv); - }, - }); - - reg.addCommand({ - path: ['config:schema'], - description: 'Print the JSON schema for the given configuration', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - format: { type: 'string' }, - merge: { type: 'boolean' }, - 'no-merge': { type: 'boolean' }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/schema'), 'default')(argv); - }, - }); - - reg.addCommand({ - path: ['config', 'schema'], - description: 'Print the JSON schema for the given configuration', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - format: { type: 'string' }, - merge: { type: 'boolean' }, - 'no-merge': { type: 'boolean' }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/schema'), 'default')(argv); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts deleted file mode 100644 index da1e04aaac..0000000000 --- a/packages/cli/src/modules/create-github-app/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; - -export default createCliPlugin({ - pluginId: 'new', - init: async reg => { - reg.addCommand({ - path: ['create-github-app'], - description: 'Create new GitHub App in your organization.', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .argument('') - .action( - lazy(() => import('./commands/create-github-app'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts deleted file mode 100644 index c14926df85..0000000000 --- a/packages/cli/src/modules/info/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 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 yargs from 'yargs'; -import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; - -export default createCliPlugin({ - pluginId: 'info', - init: async reg => { - reg.addCommand({ - path: ['info'], - description: 'Show helpful information for debugging and reporting bugs', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - include: { - type: 'string', - array: true, - default: [], - description: - 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', - }, - format: { - type: 'string', - choices: ['text', 'json'], - default: 'text', - description: 'Output format (text or json)', - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/info'), 'default')(argv); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli/src/modules/lint/commands/package/lint.ts deleted file mode 100644 index a1e45c0113..0000000000 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 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 { OptionValues } from 'commander'; -import { paths } from '../../../../lib/paths'; -import { ESLint } from 'eslint'; - -export default async (directories: string[], opts: OptionValues) => { - const eslint = new ESLint({ - cwd: paths.targetDir, - fix: opts.fix, - extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], - }); - - const results = await eslint.lintFiles( - directories.length ? directories : ['.'], - ); - - const maxWarnings = opts.maxWarnings ?? -1; - const ignoreWarnings = +maxWarnings === -1; - - const failed = - results.some(r => r.errorCount > 0) || - (!ignoreWarnings && - results.reduce((current, next) => current + next.warningCount, 0) > - maxWarnings); - - if (opts.fix) { - await ESLint.outputFixes(results); - } - - const formatter = await eslint.loadFormatter(opts.format); - - // This formatter uses the cwd to format file paths, so let's have that happen from the root instead - if (opts.format === 'eslint-formatter-friendly') { - process.chdir(paths.targetRoot); - } - - const resultText = await formatter.format(results); - - if (resultText) { - if (opts.outputFile) { - await fs.writeFile(paths.resolveTarget(opts.outputFile), resultText); - } else { - console.log(resultText); - } - } - - if (failed) { - process.exit(1); - } -}; diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts deleted file mode 100644 index 6cba232da9..0000000000 --- a/packages/cli/src/modules/lint/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; - -export function registerPackageLintCommand(command: Command) { - command.arguments('[directories...]'); - command.option('--fix', 'Attempt to automatically fix violations'); - command.option( - '--format ', - 'Lint report output format', - 'eslint-formatter-friendly', - ); - command.option( - '--output-file ', - 'Write the lint report to a file instead of stdout', - ); - command.option( - '--max-warnings ', - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - ); - command.description('Lint a package'); - command.action(lazy(() => import('./commands/package/lint'), 'default')); -} - -export default createCliPlugin({ - pluginId: 'lint', - init: async reg => { - reg.addCommand({ - path: ['package', 'lint'], - description: 'Lint a package', - execute: async ({ args }) => { - const command = new Command(); - registerPackageLintCommand(command); - - await command.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'lint'], - description: 'Lint a repository', - execute: async ({ args }) => { - const command = new Command(); - - registerPackageLintCommand(command.command('package').command('lint')); - - command.option('--fix', 'Attempt to automatically fix violations'); - command.option( - '--format ', - 'Lint report output format', - 'eslint-formatter-friendly', - ); - command.option( - '--output-file ', - 'Write the lint report to a file instead of stdout', - ); - command.option( - '--successCache', - 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', - ); - command.option( - '--successCacheDir ', - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - ); - command.option( - '--since ', - 'Only lint packages that changed since the specified ref', - ); - command.option( - '--max-warnings ', - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - ); - command.description('Lint a repository'); - command.action(lazy(() => import('./commands/repo/lint'), 'command')); - - await command.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts deleted file mode 100644 index 41192afdc4..0000000000 --- a/packages/cli/src/modules/maintenance/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 { Command } from 'commander'; -import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; - -export default createCliPlugin({ - pluginId: 'maintenance', - init: async reg => { - reg.addCommand({ - path: ['package', 'clean'], - description: 'Delete cache directories', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/clean'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'prepack'], - description: 'Prepares a package for packaging before publishing', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/pack'), 'pre'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'postpack'], - description: 'Restores the changes made by the prepack command', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/pack'), 'post'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'fix'], - description: 'Automatically fix packages in the project', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--publish', - 'Enable additional fixes that only apply when publishing packages', - ) - .option( - '--check', - 'Fail if any packages would have been changed by the command', - ) - .action(lazy(() => import('./commands/repo/fix'), 'command')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'clean'], - description: 'Delete cache and output directories', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/repo/clean'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['repo', 'list-deprecations'], - description: 'List deprecations', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option('--json', 'Output as JSON') - .action( - lazy(() => import('./commands/repo/list-deprecations'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts deleted file mode 100644 index ac56c06b7a..0000000000 --- a/packages/cli/src/modules/migrate/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2024 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 { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; - -export default createCliPlugin({ - pluginId: 'migrate', - init: async reg => { - reg.addCommand({ - path: ['versions:migrate'], - description: - 'Migrate any plugins that have been moved to the @backstage-community namespace automatically', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--pattern ', - 'Override glob for matching packages to upgrade', - ) - .option( - '--skip-code-changes', - 'Skip code changes and only update package.json files', - ) - .action(lazy(() => import('./commands/versions/migrate'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['versions:bump'], - description: 'Bump Backstage packages to the latest versions', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option( - '--pattern ', - 'Override glob for matching packages to upgrade', - ) - .option( - '--release ', - 'Bump to a specific Backstage release line or version', - 'main', - ) - .option('--skip-install', 'Skips yarn install step') - .option('--skip-migrate', 'Skips migration of any moved packages') - .action(lazy(() => import('./commands/versions/bump'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['migrate', 'package-roles'], - description: `Add package role field to packages that don't have it`, - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageRole'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['migrate', 'package-scripts'], - description: 'Set package scripts according to each package role', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageScripts'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['migrate', 'package-exports'], - description: 'Synchronize package subpath export definitions', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageExports'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['migrate', 'package-lint-configs'], - description: - 'Migrates all packages to use @backstage/cli/config/eslint-factory', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageLintConfigs'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['migrate', 'react-router-deps'], - description: - 'Migrates the react-router dependencies for all packages to be peer dependencies', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/reactRouterDeps'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts deleted file mode 100644 index a38316dd62..0000000000 --- a/packages/cli/src/modules/new/commands/new.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 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 { createNewPackage } from '../lib/createNewPackage'; - -type ArgOptions = { - option: string[]; - select?: string; - skipInstall: boolean; - private?: boolean; - npmRegistry?: string; - scope?: string; - license?: string; - baseVersion?: string; -}; - -export default async (opts: ArgOptions) => { - const { - option: rawArgOptions, - select: preselectedTemplateId, - skipInstall, - scope, - private: isPrivate, - ...otherGlobals - } = opts; - - const prefilledParams = parseParams(rawArgOptions); - - let pluginInfix: string | undefined = undefined; - let packagePrefix: string | undefined = undefined; - if (scope) { - const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`; - packagePrefix = normalizedScope.includes('/') - ? normalizedScope - : `${normalizedScope}/`; - pluginInfix = scope.includes('backstage') ? 'plugin-' : 'backstage-plugin-'; - } - - if ( - isPrivate === false || // set to false with --no-private flag - Object.values(otherGlobals).filter(Boolean).length !== 0 - ) { - console.warn( - `Global template configuration via CLI flags is deprecated, see https://backstage.io/docs/cli/new for information on how to configure package templating`, - ); - } - - await createNewPackage({ - prefilledParams, - preselectedTemplateId, - configOverrides: { - license: otherGlobals.license, - version: otherGlobals.baseVersion, - private: isPrivate, - publishRegistry: otherGlobals.npmRegistry, - packageNamePrefix: packagePrefix, - packageNamePluginInfix: pluginInfix, - }, - skipInstall, - }); -}; - -function parseParams(optionStrings: string[]): Record { - const options: Record = {}; - - for (const str of optionStrings) { - const [key] = str.split('=', 1); - const value = str.slice(key.length + 1); - if (!key || str[key.length] !== '=') { - throw new Error( - `Invalid option '${str}', must be of the format =`, - ); - } - options[key] = value; - } - - return options; -} diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts deleted file mode 100644 index 120e77c7c0..0000000000 --- a/packages/cli/src/modules/new/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; -import { NotImplementedError } from '@backstage/errors'; - -export default createCliPlugin({ - pluginId: 'new', - init: async reg => { - reg.addCommand({ - path: ['new'], - description: - 'Open up an interactive guide to creating new things in your app', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .storeOptionsAsProperties(false) - .description( - 'Open up an interactive guide to creating new things in your app', - ) - .option( - '--select ', - 'Select the thing you want to be creating upfront', - ) - .option( - '--option =', - 'Pre-fill options for the creation process', - (opt, arr: string[]) => [...arr, opt], - [], - ) - .option( - '--skip-install', - `Skips running 'yarn install' and 'yarn lint --fix'`, - ) - .option('--scope ', 'The scope to use for new packages') - .option( - '--npm-registry ', - 'The package registry to use for new packages', - ) - .option( - '--baseVersion ', - 'The version to use for any new packages (default: 0.1.0)', - ) - .option( - '--license ', - 'The license to use for any new packages (default: Apache-2.0)', - ) - .option('--no-private', 'Do not mark new packages as private') - .action(lazy(() => import('./commands/new'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['create'], - description: 'Create a new Backstage app', - deprecated: true, - execute: async () => { - throw new NotImplementedError( - `This command has been removed, use 'backstage-cli new' instead`, - ); - }, - }); - reg.addCommand({ - path: ['create-plugin'], - description: 'Create a new Backstage plugin', - deprecated: true, - execute: async () => { - throw new NotImplementedError( - `This command has been removed, use 'backstage-cli new' instead`, - ); - }, - }); - }, -}); diff --git a/packages/cli/src/modules/test/commands/repo/test.test.ts b/packages/cli/src/modules/test/commands/repo/test.test.ts deleted file mode 100644 index d2f2485717..0000000000 --- a/packages/cli/src/modules/test/commands/repo/test.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 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 { createFlagFinder } from './test'; - -describe('createFlagFinder', () => { - it('finds flags', () => { - const find = createFlagFinder([ - '--foo', - '--no-bar', - '-b', - '-c', - '--baz=1', - '--qux', - '2', - '-de', - ]); - - expect( - find('--foo', '--bar', '-b', '-c', '--baz', '--qux', '-d', '-e'), - ).toBe(true); - expect(find('--foo')).toBe(true); - expect(find('--bar')).toBe(true); - expect(find('--no-bar')).toBe(false); - expect(find('-a')).toBe(false); - expect(find('-b')).toBe(true); - expect(find('-c')).toBe(true); - expect(find('-d')).toBe(true); - expect(find('-e')).toBe(true); - expect(find('--baz')).toBe(true); - expect(find('--qux')).toBe(true); - expect(find('--qux')).toBe(true); - }); -}); diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts deleted file mode 100644 index 1583c74997..0000000000 --- a/packages/cli/src/modules/test/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024 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 { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; - -export default createCliPlugin({ - pluginId: 'test', - init: async reg => { - reg.addCommand({ - path: ['repo', 'test'], - description: - 'Run tests, forwarding args to Jest, defaulting to watch mode', - execute: async ({ args }) => { - const command = new Command(); - command.allowUnknownOption(true); - command.option( - '--since ', - 'Only test packages that changed since the specified ref', - ); - command.option('--successCache', 'Enable success caching'); - command.option( - '--successCacheDir ', - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - ); - command.option( - '--jest-help', - 'Show help for Jest CLI options, which are passed through', - ); - command.action(lazy(() => import('./commands/repo/test'), 'command')); - await command.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'test'], - description: - 'Run tests, forwarding args to Jest, defaulting to watch mode', - execute: async ({ args }) => { - const command = new Command(); - - command.allowUnknownOption(true); - command.helpOption(', --backstage-cli-help'); - command.action( - lazy(() => import('./commands/package/test'), 'default'), - ); - await command.parseAsync(args, { from: 'user' }); - }, - }); - }, -}); diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index 9ee252cfc5..703ec972fc 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -15,10 +15,28 @@ */ import { CliInitializer } from './CliInitializer'; -import { createCliPlugin } from './factory'; +import { createCliModule } from './factory'; process.exit = jest.fn() as any; +describe('createCliModule', () => { + it('should throw if packageJson has no name', () => { + expect(() => + createCliModule({ + packageJson: { name: '' }, + init: async () => {}, + }), + ).toThrow('The packageJson provided to createCliModule must have a name'); + + expect(() => + createCliModule({ + packageJson: {} as any, + init: async () => {}, + }), + ).toThrow('The packageJson provided to createCliModule must have a name'); + }); +}); + describe('CliInitializer', () => { beforeEach(() => { jest.resetAllMocks(); @@ -28,8 +46,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -50,8 +68,8 @@ describe('CliInitializer', () => { process.argv = ['node', 'cli', 'test', '[positional]', '']; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test'], @@ -67,6 +85,149 @@ describe('CliInitializer', () => { expect(process.exit).toHaveBeenCalledWith(0); }); + it('should run commands using a loader', async () => { + expect.assertions(2); + process.argv = ['node', 'cli', 'test', '--verbose']; + const initializer = new CliInitializer(); + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/test' }, + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'test', + execute: { + loader: async () => ({ + default: async ({ args }) => { + expect(args).toEqual(['--verbose']); + }, + }), + }, + }), + }), + ); + await initializer.run(); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should run experimental commands but exclude them from help output', async () => { + expect.assertions(3); + process.argv = ['node', 'cli', 'secret']; + const initializer = new CliInitializer(); + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/test' }, + init: async reg => { + reg.addCommand({ + path: ['visible'], + description: 'A visible command', + execute: () => Promise.resolve(), + }); + reg.addCommand({ + path: ['secret'], + description: 'An experimental command', + experimental: true, + execute: ({ args }) => { + expect(args).toEqual([]); + return Promise.resolve(); + }, + }); + }, + }), + ); + await initializer.run(); + expect(process.exit).toHaveBeenCalledWith(0); + + process.argv = ['node', 'cli', '--help']; + const writeSpy = jest.spyOn(process.stdout, 'write'); + const initializer2 = new CliInitializer(); + initializer2.add( + createCliModule({ + packageJson: { name: '@backstage/test' }, + init: async reg => { + reg.addCommand({ + path: ['visible'], + description: 'A visible command', + execute: () => Promise.resolve(), + }); + reg.addCommand({ + path: ['secret'], + description: 'An experimental command', + experimental: true, + execute: () => Promise.resolve(), + }); + }, + }), + ); + await initializer2.run(); + const helpOutput = writeSpy.mock.calls.map(c => c[0]).join(''); + expect(helpOutput).not.toContain('secret'); + writeSpy.mockRestore(); + }); + + it('should hide tree nodes when all children are experimental', async () => { + process.argv = ['node', 'cli', '--help']; + const writeSpy = jest.spyOn(process.stdout, 'write'); + const initializer = new CliInitializer(); + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/test' }, + init: async reg => { + reg.addCommand({ + path: ['visible'], + description: 'A visible command', + execute: () => Promise.resolve(), + }); + reg.addCommand({ + path: ['group', 'alpha'], + description: 'First experimental command', + experimental: true, + execute: () => Promise.resolve(), + }); + reg.addCommand({ + path: ['group', 'beta'], + description: 'Second experimental command', + experimental: true, + execute: () => Promise.resolve(), + }); + }, + }), + ); + await initializer.run(); + const helpOutput = writeSpy.mock.calls.map(c => c[0]).join(''); + expect(helpOutput).toContain('visible'); + expect(helpOutput).not.toContain('group'); + writeSpy.mockRestore(); + }); + + it('should show tree nodes when some children are visible', async () => { + process.argv = ['node', 'cli', '--help']; + const writeSpy = jest.spyOn(process.stdout, 'write'); + const initializer = new CliInitializer(); + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/test' }, + init: async reg => { + reg.addCommand({ + path: ['group', 'alpha'], + description: 'A visible nested command', + execute: () => Promise.resolve(), + }); + reg.addCommand({ + path: ['group', 'beta'], + description: 'An experimental nested command', + experimental: true, + execute: () => Promise.resolve(), + }); + }, + }), + ); + await initializer.run(); + const helpOutput = writeSpy.mock.calls.map(c => c[0]).join(''); + expect(helpOutput).toContain('group'); + writeSpy.mockRestore(); + }); + it('should pass positional args to the subcommand if nested', async () => { expect.assertions(2); process.argv = [ @@ -80,8 +241,8 @@ describe('CliInitializer', () => { ]; const initializer = new CliInitializer(); initializer.add( - createCliPlugin({ - pluginId: 'test', + createCliModule({ + packageJson: { name: '@backstage/test' }, init: async reg => reg.addCommand({ path: ['test', 'nested', 'command'], @@ -96,4 +257,120 @@ describe('CliInitializer', () => { await initializer.run(); expect(process.exit).toHaveBeenCalledWith(0); }); + + it('should silently override array-sourced module with conflicting individual module while keeping siblings', async () => { + expect.assertions(3); + process.argv = ['node', 'cli', 'test']; + const initializer = new CliInitializer(); + + const individualModule = createCliModule({ + packageJson: { name: '@backstage/individual' }, + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'individual test', + execute: ({ args }) => { + expect(args).toEqual([]); + return Promise.resolve(); + }, + }), + }); + + const conflictingArrayModule = createCliModule({ + packageJson: { name: '@backstage/array-conflict' }, + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'array test (should be skipped)', + execute: () => Promise.resolve(), + }), + }); + + const nonConflictingArrayModule = createCliModule({ + packageJson: { name: '@backstage/array-sibling' }, + init: async reg => + reg.addCommand({ + path: ['other'], + description: 'other command', + execute: () => Promise.resolve(), + }), + }); + + initializer.add(individualModule); + initializer.add([conflictingArrayModule, nonConflictingArrayModule]); + + await initializer.run(); + expect(process.exit).toHaveBeenCalledWith(0); + + // Verify the sibling command is available by running it + process.argv = ['node', 'cli', 'other']; + const initializer2 = new CliInitializer(); + initializer2.add(individualModule); + initializer2.add([conflictingArrayModule, nonConflictingArrayModule]); + await initializer2.run(); + expect(process.exit).toHaveBeenCalledTimes(2); + }); + + it('should error with package names when two individual modules conflict', async () => { + const initializer = new CliInitializer(); + + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/module-a' }, + init: async reg => + reg.addCommand({ + path: ['conflicting'], + description: 'from module A', + execute: () => Promise.resolve(), + }), + }), + ); + + initializer.add( + createCliModule({ + packageJson: { name: '@backstage/module-b' }, + init: async reg => + reg.addCommand({ + path: ['conflicting'], + description: 'from module B', + execute: () => Promise.resolve(), + }), + }), + ); + + await expect(initializer.run()).rejects.toThrow( + 'Command "conflicting" from "@backstage/module-b" conflicts with an existing command from "@backstage/module-a"', + ); + }); + + it('should error with package names when two array-sourced modules conflict', async () => { + const initializer = new CliInitializer(); + + const moduleA = createCliModule({ + packageJson: { name: '@backstage/array-a' }, + init: async reg => + reg.addCommand({ + path: ['shared'], + description: 'from array A', + execute: () => Promise.resolve(), + }), + }); + + const moduleB = createCliModule({ + packageJson: { name: '@backstage/array-b' }, + init: async reg => + reg.addCommand({ + path: ['shared'], + description: 'from array B', + execute: () => Promise.resolve(), + }), + }); + + initializer.add([moduleA]); + initializer.add([moduleB]); + + await expect(initializer.run()).rejects.toThrow( + 'Command "shared" from "@backstage/array-b" conflicts with an existing command from "@backstage/array-a"', + ); + }); }); diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index eab7961b40..adbdfbe517 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,44 +15,95 @@ */ import { CommandGraph } from './CommandGraph'; -import { CliFeature, OpaqueCliPlugin } from './types'; -import { CommandRegistry } from './CommandRegistry'; +import { + OpaqueCliModule, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + isCommandNodeHidden, +} from '@internal/cli'; +import type { CliModule } from '@backstage/cli-node'; import { Command } from 'commander'; -import { version } from '../lib/version'; +import { version } from './version'; import chalk from 'chalk'; -import { exitWithError } from '../lib/errors'; +import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; -type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>; +type UninitializedFeature = + | CliModule + | CliModule[] + | Promise<{ default: CliModule | CliModule[] }>; + +interface TaggedFeature { + feature: CliModule; + /** + * Whether this module was sourced from an array (e.g. cli-defaults). + * Array-sourced modules are silently skipped when any of their commands + * overlap with an individually-added module, allowing explicit module + * additions to take precedence without causing conflicts. + */ + fromArray: boolean; +} export class CliInitializer { private graph = new CommandGraph(); - private commandRegistry = new CommandRegistry(this.graph); - #uninitiazedFeatures: Promise[] = []; + #uninitiazedFeatures: Promise[] = []; add(feature: UninitializedFeature) { if (isPromise(feature)) { this.#uninitiazedFeatures.push( - feature.then(f => unwrapFeature(f.default)), + feature.then(f => { + const unwrapped = unwrapFeature(f.default); + if (Array.isArray(unwrapped)) { + return unwrapped.map(m => ({ feature: m, fromArray: true })); + } + return [{ feature: unwrapped, fromArray: false }]; + }), + ); + } else if (Array.isArray(feature)) { + this.#uninitiazedFeatures.push( + Promise.resolve(feature.map(m => ({ feature: m, fromArray: true }))), ); } else { - this.#uninitiazedFeatures.push(Promise.resolve(feature)); + this.#uninitiazedFeatures.push( + Promise.resolve([{ feature, fromArray: false }]), + ); } } - async #register(feature: CliFeature) { - if (OpaqueCliPlugin.isType(feature)) { - const internal = OpaqueCliPlugin.toInternal(feature); - await internal.init(this.commandRegistry); + async #register(feature: CliModule) { + if (OpaqueCliModule.isType(feature)) { + for (const command of await OpaqueCliModule.toInternal(feature) + .commands) { + this.graph.add(command, feature); + } } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } } async #doInit() { - const features = await Promise.all(this.#uninitiazedFeatures); - for (const feature of features) { + const resolvedGroups = await Promise.all(this.#uninitiazedFeatures); + const allFeatures = resolvedGroups.flat(); + + // Collect command paths from individually-added modules + const individualPaths = new Set(); + for (const { feature, fromArray } of allFeatures) { + if (!fromArray && OpaqueCliModule.isType(feature)) { + const cmds = await OpaqueCliModule.toInternal(feature).commands; + for (const cmd of cmds) { + individualPaths.add(cmd.path.join(' ')); + } + } + } + + for (const { feature, fromArray } of allFeatures) { + if (fromArray && OpaqueCliModule.isType(feature)) { + const cmds = await OpaqueCliModule.toInternal(feature).commands; + if (cmds.some(cmd => individualPaths.has(cmd.path.join(' ')))) { + continue; + } + } await this.#register(feature); } } @@ -78,21 +129,28 @@ export class CliInitializer { })); while (queue.length) { const { node, argParser } = queue.shift()!; - if (node.$$type === '@tree/root') { + if (OpaqueCommandTreeNode.isType(node)) { + const internal = OpaqueCommandTreeNode.toInternal(node); const treeParser = argParser - .command(`${node.name} [command]`) - .description(node.name); + .command(`${internal.name} [command]`, { + hidden: isCommandNodeHidden(node), + }) + .description(internal.name); queue.push( - ...node.children.map(child => ({ + ...internal.children.map(child => ({ node: child, argParser: treeParser, })), ); } else { + const internal = OpaqueCommandLeafNode.toInternal(node); argParser - .command(node.name, { hidden: !!node.command.deprecated }) - .description(node.command.description) + .command(internal.name, { + hidden: + !!internal.command.deprecated || !!internal.command.experimental, + }) + .description(internal.command.description) .helpOption(false) .allowUnknownOption(true) .allowExcessArguments(true) @@ -111,20 +169,32 @@ export class CliInitializer { // Skip the command name if ( argIndex === index && - node.command.path[argIndex] === nonProcessArgs[argIndex] + internal.command.path[argIndex] === nonProcessArgs[argIndex] ) { index += 1; continue; } positionalArgs.push(nonProcessArgs[argIndex]); } - await node.command.execute({ + const context = { args: [...positionalArgs, ...args.unknown], info: { - usage: [programName, ...node.command.path].join(' '), - description: node.command.description, + usage: [programName, ...internal.command.path].join(' '), + name: internal.command.path.join(' '), }, - }); + }; + + if (typeof internal.command.execute === 'function') { + await internal.command.execute(context); + } else { + const mod = await internal.command.execute.loader(); + // Handle CJS double-wrapping of default exports + const fn = + typeof mod.default === 'function' + ? mod.default + : (mod.default as any).default; + await fn(context); + } process.exit(0); } catch (error: unknown) { exitWithError(error); @@ -144,14 +214,18 @@ export class CliInitializer { exitWithError(new ForwardedError('Unhandled rejection', rejection)); }); - program.parse(process.argv); + await program.parseAsync(process.argv); } } /** @internal */ export function unwrapFeature( - feature: CliFeature | { default: CliFeature }, -): CliFeature { + feature: CliModule | CliModule[] | { default: CliModule | CliModule[] }, +): CliModule | CliModule[] { + if (Array.isArray(feature)) { + return feature; + } + if ('$$type' in feature) { return feature; } diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts index a0a2de3305..b201a3fba3 100644 --- a/packages/cli/src/wiring/CommandGraph.ts +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -13,91 +13,146 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BackstageCommand } from './types'; - -type Node = TreeNode | LeafNode; - -interface TreeNode { - $$type: '@tree/root'; - name: string; - children: TreeNode[]; -} - -interface LeafNode { - $$type: '@tree/leaf'; - name: string; - command: BackstageCommand; -} +import { + CommandNode, + OpaqueCommandTreeNode, + OpaqueCommandLeafNode, + OpaqueCliModule, +} from '@internal/cli'; +import { CliCommand, CliModule } from './types'; /** * A sparse graph of commands. */ export class CommandGraph { - private graph: Node[] = []; + private graph: CommandNode[] = []; /** * Adds a command to the graph. The graph is sparse, so we use the path to determine the nodes * to traverse. Only leaf nodes should have a command/action. */ - add(command: BackstageCommand) { + add(command: CliCommand, module?: CliModule) { const path = command.path; let current = this.graph; for (let i = 0; i < path.length - 1; i++) { const name = path[i]; - let next = current.find(n => n.name === name); + let next = current.find( + n => + (OpaqueCommandTreeNode.isType(n) && + OpaqueCommandTreeNode.toInternal(n).name === name) || + (OpaqueCommandLeafNode.isType(n) && + OpaqueCommandLeafNode.toInternal(n).name === name), + ); if (!next) { - next = { $$type: '@tree/root', name, children: [] }; + next = OpaqueCommandTreeNode.createInstance('v1', { + name, + children: [], + }); current.push(next); - } else if (next.$$type === '@tree/leaf') { + } else if (OpaqueCommandLeafNode.isType(next)) { throw new Error( - `Command already exists at path: "${path.slice(0, i).join(' ')}"`, + formatConflictError( + path, + module, + OpaqueCommandLeafNode.toInternal(next).module, + ), ); } - current = next.children; + current = OpaqueCommandTreeNode.toInternal(next).children; } - const last = current.find(n => n.name === path[path.length - 1]); - if (last && last.$$type === '@tree/leaf') { + const lastName = path[path.length - 1]; + const last = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === lastName; + } + return OpaqueCommandLeafNode.toInternal(n).name === lastName; + }); + if (last && OpaqueCommandLeafNode.isType(last)) { throw new Error( - `Command already exists at path: "${path.slice(0, -1).join(' ')}"`, + formatConflictError( + path, + module, + OpaqueCommandLeafNode.toInternal(last).module, + ), ); } else { - current.push({ - $$type: '@tree/leaf', - name: path[path.length - 1], - command, - }); + current.push( + OpaqueCommandLeafNode.createInstance('v1', { + name: lastName, + command, + module, + }), + ); } } /** * Given a path, try to find a command that matches it. */ - find(path: string[]): BackstageCommand | undefined { + find(path: string[]): CliCommand | undefined { let current = this.graph; for (let i = 0; i < path.length - 1; i++) { const name = path[i]; - const next = current.find(n => n.name === name); - if (!next) { - return undefined; - } else if (next.$$type === '@tree/leaf') { + const next = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === name; + } + return OpaqueCommandLeafNode.toInternal(n).name === name; + }); + if (!next || OpaqueCommandLeafNode.isType(next)) { return undefined; } - current = next.children; + current = OpaqueCommandTreeNode.toInternal(next).children; } - const last = current.find(n => n.name === path[path.length - 1]); - if (!last || last.$$type === '@tree/root') { + const lastName = path[path.length - 1]; + const last = current.find(n => { + if (OpaqueCommandTreeNode.isType(n)) { + return OpaqueCommandTreeNode.toInternal(n).name === lastName; + } + return OpaqueCommandLeafNode.toInternal(n).name === lastName; + }); + if (!last || OpaqueCommandTreeNode.isType(last)) { return undefined; } - return last?.command; + return OpaqueCommandLeafNode.toInternal(last).command; } - atDepth(depth: number): Node[] { + atDepth(depth: number): CommandNode[] { let current = this.graph; for (let i = 0; i < depth; i++) { current = current.flatMap(n => - n.$$type === '@tree/root' ? n.children : [], + OpaqueCommandTreeNode.isType(n) + ? OpaqueCommandTreeNode.toInternal(n).children + : [], ); } return current; } } + +function getModuleName(module?: CliModule): string | undefined { + if (module && OpaqueCliModule.isType(module)) { + return OpaqueCliModule.toInternal(module).packageName; + } + return undefined; +} + +function formatConflictError( + path: string[], + newModule?: CliModule, + existingModule?: CliModule, +): string { + const cmd = path.join(' '); + const newPkg = getModuleName(newModule); + const existingPkg = getModuleName(existingModule); + if (newPkg && existingPkg) { + return `Command "${cmd}" from "${newPkg}" conflicts with an existing command from "${existingPkg}"`; + } + if (newPkg) { + return `Command "${cmd}" from "${newPkg}" conflicts with an existing command`; + } + if (existingPkg) { + return `Command "${cmd}" conflicts with an existing command from "${existingPkg}"`; + } + return `Command "${cmd}" conflicts with an existing command`; +} diff --git a/packages/cli/src/wiring/CommandRegistry.ts b/packages/cli/src/wiring/CommandRegistry.ts index 9e8ff9646c..c3c1f1956d 100644 --- a/packages/cli/src/wiring/CommandRegistry.ts +++ b/packages/cli/src/wiring/CommandRegistry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { CommandGraph } from './CommandGraph'; -import { BackstageCommand } from './types'; +import { CliCommand } from './types'; export class CommandRegistry { private graph: CommandGraph; @@ -22,7 +22,7 @@ export class CommandRegistry { this.graph = graph; } - addCommand(command: BackstageCommand) { + addCommand(command: CliCommand) { this.graph.add(command); } } diff --git a/packages/cli/src/wiring/describeParentCallSite.ts b/packages/cli/src/wiring/describeParentCallSite.ts deleted file mode 100644 index 35603e33b0..0000000000 --- a/packages/cli/src/wiring/describeParentCallSite.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2023 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. - */ - -// Single re-export to avoid doing this import in multiple places, but still -// avoid duplicate declarations because this one is a bit tricky. -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; diff --git a/packages/cli/src/wiring/discoverCliModules.ts b/packages/cli/src/wiring/discoverCliModules.ts new file mode 100644 index 0000000000..e2945d2631 --- /dev/null +++ b/packages/cli/src/wiring/discoverCliModules.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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 { targetPaths } from '@backstage/cli-common'; +import { PackageRoles } from '@backstage/cli-node'; +import fs from 'node:fs'; +import { resolve as resolvePath } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +/** + * Scans the target project root's package.json for dependencies that are CLI + * modules (packages with `backstage.role === 'cli-module'`). + * + * Returns the resolved entry point paths of discovered CLI module packages, + * or an empty array if none are found or the project root cannot be read. + * The paths are resolved relative to the project root to ensure they can be + * imported regardless of where the CLI code itself is located. + */ +export function discoverCliModules(): string[] { + const rootDir = targetPaths.rootDir; + const pkgJsonPath = resolvePath(rootDir, 'package.json'); + + let projectPkg: { + dependencies?: Record; + devDependencies?: Record; + }; + try { + projectPkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); + } catch { + return []; + } + + const allDeps = { + ...projectPkg.dependencies, + ...projectPkg.devDependencies, + }; + + const modules: string[] = []; + + for (const depName of Object.keys(allDeps)) { + try { + const depPkgPath = require.resolve(`${depName}/package.json`, { + paths: [rootDir], + }); + const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8')); + if (PackageRoles.getRoleFromPackage(depPkg) === 'cli-module') { + const resolvedPath = require.resolve(depName, { paths: [rootDir] }); + modules.push(pathToFileURL(resolvedPath).href); + } + } catch { + // Skip packages that can't be resolved or read + } + } + + return modules; +} diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/wiring/errors.ts similarity index 100% rename from packages/cli/src/lib/errors.ts rename to packages/cli/src/wiring/errors.ts diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index 6374f37c14..f8f04715c3 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,18 +14,4 @@ * limitations under the License. */ -import { describeParentCallSite } from './describeParentCallSite'; -import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; - -export function createCliPlugin(options: { - pluginId: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; -}): CliPlugin { - return OpaqueCliPlugin.createInstance('v1', { - pluginId: options.pluginId, - init: options.init, - description: `created at '${describeParentCallSite()}'`, - }); -} +export { createCliModule } from '@backstage/cli-node'; diff --git a/packages/cli/src/lib/lazy.ts b/packages/cli/src/wiring/lazy.ts similarity index 96% rename from packages/cli/src/lib/lazy.ts rename to packages/cli/src/wiring/lazy.ts index d1255ea1bc..64a4d43ef4 100644 --- a/packages/cli/src/lib/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -15,7 +15,7 @@ */ import { assertError } from '@backstage/errors'; -import { exitWithError } from '../lib/errors'; +import { exitWithError } from './errors'; type ActionFunc = (...args: any[]) => Promise; type ActionExports = { diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index fc2c2b454d..7a8a009a60 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -13,44 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpaqueType } from '@internal/opaque'; -export interface BackstageCommand { - path: string[]; - description: string; - deprecated?: boolean; - execute: (context: { - args: string[]; - info: { - /** - * The usage string of the current command, for example: "backstage-cli repo test" - */ - usage: string; - /** - * The description provided for the command - */ - description: string; - }; - }) => Promise; -} - -export type CliFeature = CliPlugin; - -export interface CliPlugin { - readonly pluginId: string; - readonly $$type: '@backstage/CliPlugin'; -} - -export const OpaqueCliPlugin = OpaqueType.create<{ - public: CliPlugin; - versions: { - readonly version: 'v1'; - readonly description: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; - }; -}>({ - type: '@backstage/CliPlugin', - versions: ['v1'], -}); +export type { + CliCommandContext, + CliCommand, + CliModule, +} from '@backstage/cli-node'; diff --git a/packages/cli/src/wiring/version.ts b/packages/cli/src/wiring/version.ts new file mode 100644 index 0000000000..d7d0b3a2fe --- /dev/null +++ b/packages/cli/src/wiring/version.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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 { findOwnPaths } from '@backstage/cli-common'; + +/* eslint-disable-next-line no-restricted-syntax */ +const ownPaths = findOwnPaths(__dirname); + +export function findVersion() { + const pkgContent = fs.readFileSync(ownPaths.resolve('package.json'), 'utf8'); + return JSON.parse(pkgContent).version; +} + +export const version = findVersion(); +export const isDev = fs.pathExistsSync(ownPaths.resolve('src')); diff --git a/packages/cli/templates/new-frontend-plugin/dev/index.tsx b/packages/cli/templates/new-frontend-plugin/dev/index.tsx index e1bcb0401e..21f842925b 100644 --- a/packages/cli/templates/new-frontend-plugin/dev/index.tsx +++ b/packages/cli/templates/new-frontend-plugin/dev/index.tsx @@ -1,10 +1,5 @@ -import { createApp } from '@backstage/frontend-defaults'; -import ReactDOM from 'react-dom'; +import { createDevApp } from '@backstage/frontend-dev-utils'; import plugin from '../src'; -const app = createApp({ - features: [plugin], -}); - -ReactDOM.render(app.createRoot(), document.getElementById('root')); +createDevApp({ features: [plugin] }); diff --git a/packages/cli/templates/new-frontend-plugin/package.json.hbs b/packages/cli/templates/new-frontend-plugin/package.json.hbs index 7ee1aed5e9..4a37a4fa63 100644 --- a/packages/cli/templates/new-frontend-plugin/package.json.hbs +++ b/packages/cli/templates/new-frontend-plugin/package.json.hbs @@ -35,7 +35,7 @@ }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@backstage/frontend-defaults": "{{versionQuery '@backstage/frontend-defaults'}}", + "@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'}}", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 46139b6d98..38ccd0f874 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/codemods +## 0.1.55-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## 0.1.54 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/cli-common@0.1.18 + ## 0.1.54-next.0 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index cdb623c4df..c531c4ee69 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/codemods", - "version": "0.1.54-next.0", + "version": "0.1.55-next.0", "description": "A collection of codemods for Backstage projects", "backstage": { "role": "cli" @@ -45,7 +45,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", - "commander": "^12.0.0", + "commander": "^14.0.3", "jscodeshift": "^0.16.0", "jscodeshift-add-imports": "^1.0.10" }, diff --git a/packages/codemods/src/action.ts b/packages/codemods/src/action.ts index 2c7b9b8d01..7a6c241497 100644 --- a/packages/codemods/src/action.ts +++ b/packages/codemods/src/action.ts @@ -16,17 +16,16 @@ import { relative as relativePath } from 'node:path'; import { OptionValues } from 'commander'; -import { findPaths, run } from '@backstage/cli-common'; +import { findOwnPaths, run } from '@backstage/cli-common'; import { platform } from 'node:os'; -// eslint-disable-next-line no-restricted-syntax -const paths = findPaths(__dirname); - export function createCodemodAction(name: string) { return async (dirs: string[], opts: OptionValues) => { + // eslint-disable-next-line no-restricted-syntax + const paths = findOwnPaths(__dirname); const transformPath = relativePath( process.cwd(), - paths.resolveOwn('transforms', `${name}.js`), + paths.resolve('transforms', `${name}.js`), ); const args = [ diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 2c2f214aef..22a9f2c76f 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/config-loader +## 1.10.9-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 1.10.8 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/cli-common@0.1.18 + ## 1.10.8-next.0 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 22c1bd7910..18b16787ab 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.8-next.0", + "version": "1.10.9-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config-loader/report.api.md b/packages/config-loader/report.api.md index e2741d609c..a13904f14b 100644 --- a/packages/config-loader/report.api.md +++ b/packages/config-loader/report.api.md @@ -221,7 +221,7 @@ export interface MutableConfigSourceOptions { } // @public -export type Parser = ({ contents }: { contents: string }) => Promise<{ +export type Parser = (input: { contents: string }) => Promise<{ result?: JsonObject; }>; diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 2133eccc29..153da69dbd 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -27,7 +27,7 @@ import { } from './RemoteConfigSource'; import { ConfigSource, SubstitutionFunc } from './types'; import { ObservableConfigProxy } from './ObservableConfigProxy'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; /** * A target to read configuration from. @@ -157,7 +157,7 @@ export class ConfigSources { static defaultForTargets( options: ConfigSourcesDefaultForTargetsOptions, ): ConfigSource { - const rootDir = options.rootDir ?? findPaths(process.cwd()).targetRoot; + const rootDir = options.rootDir ?? targetPaths.rootDir; const argSources = options.targets.map(arg => { if (arg.type === 'url') { diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 8215d7f96f..b7d03cfea2 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/core-app-api +## 1.19.6-next.1 + +### Patch Changes + +- 12d8afe: Added `BUIProvider` from `@backstage/ui` to the app shell provider tree, enabling BUI components to fire analytics events through the Backstage analytics system. +- 0452d02: Add optional `description` field to plugin-level feature flags. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + +## 1.19.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## 1.19.5 + +### Patch Changes + +- 5a71e7a: Fixed memory leak caused by duplicate `AppThemeSelector` instances and missing cleanup in `AppThemeSelector` and `AppLanguageSelector`. Added `dispose()` method to both selectors for proper resource cleanup. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 1.19.5-next.1 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 2a78d63144..c37d4172a4 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-app-api", - "version": "1.19.5-next.1", + "version": "1.19.6-next.1", "description": "Core app API used by Backstage apps", "backstage": { "role": "web-library" @@ -49,6 +49,7 @@ "@backstage/config": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/types": "workspace:^", + "@backstage/ui": "workspace:^", "@backstage/version-bridge": "workspace:^", "@types/prop-types": "^15.7.3", "history": "^5.0.0", diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index 4273587c00..0a899ff88a 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -114,12 +114,16 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^API context is not available/); + }).toThrow('No implementation available for apiRef{x}'); }).error, ).toEqual([ - expect.stringContaining('Error: API context is not available'), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), expect.objectContaining({ type: 'unhandled-exception' }), - expect.stringContaining('Error: API context is not available'), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), expect.objectContaining({ type: 'unhandled-exception' }), expect.stringContaining( 'The above error occurred in the component', @@ -130,12 +134,16 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow(/^API context is not available/); + }).toThrow('No implementation available for apiRef{x}'); }).error, ).toEqual([ - expect.stringContaining('Error: API context is not available'), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), expect.objectContaining({ type: 'unhandled-exception' }), - expect.stringContaining('Error: API context is not available'), + expect.stringContaining( + 'Error: No implementation available for apiRef{x}', + ), expect.objectContaining({ type: 'unhandled-exception' }), expect.stringContaining( 'The above error occurred in the component', diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index bfaeb2f2db..4f0d2b2d4d 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -45,7 +45,9 @@ import { fetchApiRef, discoveryApiRef, errorApiRef, + useAnalytics, } from '@backstage/core-plugin-api'; +import { BUIProvider } from '@backstage/ui'; import { AppLanguageApi, appLanguageApiRef, @@ -339,6 +341,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be for (const flag of plugin.getFeatureFlags()) { featureFlagsApi.registerFlag({ name: flag.name, + description: flag.description, pluginId: plugin.getId(), }); } @@ -389,26 +392,28 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be return ( - - - - + + + - }>{children} - - - - + + }>{children} + + + + + ); }; diff --git a/packages/core-app-api/src/app/AppRouter.tsx b/packages/core-app-api/src/app/AppRouter.tsx index afc87d6753..337792d71b 100644 --- a/packages/core-app-api/src/app/AppRouter.tsx +++ b/packages/core-app-api/src/app/AppRouter.tsx @@ -23,7 +23,9 @@ import { SignInPageProps, useApi, useApp, + useAnalytics, } from '@backstage/core-plugin-api'; +import { BUIProvider } from '@backstage/ui'; import { InternalAppContext } from './InternalAppContext'; import { isReactRouterBeta } from './isReactRouterBeta'; import { RouteTracker } from '../routing/RouteTracker'; @@ -143,18 +145,22 @@ export function AppRouter(props: AppRouterProps) { if (isReactRouterBeta()) { return ( - - - {props.children}} /> - + + + + {props.children}} /> + + ); } return ( - - {props.children} + + + {props.children} + ); } @@ -162,28 +168,32 @@ export function AppRouter(props: AppRouterProps) { if (isReactRouterBeta()) { return ( - - - - {props.children}} /> - - + + + + + {props.children}} /> + + + ); } return ( - - - {props.children} - + + + + {props.children} + + ); } diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 7268ae63be..e630f0401d 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/core-compat-api +## 0.5.9-next.2 + +### Patch Changes + +- b15a685: Added `withApis`, which is a Higher-Order Component for providing APIs as props to a component via `useApiHolder`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/plugin-app-react@0.2.1-next.1 + +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## 0.5.8 + +### Patch Changes + +- c38b74d: Internal updates for blueprint moves to `@backstage/plugin-app-react`. +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 0.5.8-next.2 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index bd1309873a..152b6d1141 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.8-next.2", + "version": "0.5.9-next.2", "backstage": { "role": "web-library" }, @@ -32,6 +32,7 @@ }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-app-react": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", diff --git a/packages/core-compat-api/report.api.md b/packages/core-compat-api/report.api.md index ae202c96f7..13d0849000 100644 --- a/packages/core-compat-api/report.api.md +++ b/packages/core-compat-api/report.api.md @@ -22,11 +22,13 @@ import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSX as JSX_3 } from 'react/jsx-runtime'; +import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; import { SubRouteRef as SubRouteRef_2 } from '@backstage/frontend-plugin-api'; +import { TypesToApiRefs } from '@backstage/frontend-plugin-api'; // @public export function compatWrapper(element: ReactNode): JSX_3.Element; @@ -143,5 +145,15 @@ export type ToNewRouteRef = ? ExternalRouteRef_2 : never; +// @public +export function withApis( + apis: TypesToApiRefs, +): ( + WrappedComponent: ComponentType, +) => { + (props: PropsWithChildren>): JSX_3.Element; + displayName: string; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-compat-api/src/collectEntityPageContents.test.tsx b/packages/core-compat-api/src/collectEntityPageContents.test.tsx index fd74a7e89b..5ee1439360 100644 --- a/packages/core-compat-api/src/collectEntityPageContents.test.tsx +++ b/packages/core-compat-api/src/collectEntityPageContents.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ExtensionAttachToSpec } from '@backstage/frontend-plugin-api'; +import { ExtensionAttachTo } from '@backstage/frontend-plugin-api'; import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog'; import { JSX } from 'react'; import { collectEntityPageContents } from './collectEntityPageContents'; @@ -73,7 +73,7 @@ const otherTestContent = ( function collect(element: JSX.Element) { const result = new Array<{ id: string; - attachTo: ExtensionAttachToSpec; + attachTo: ExtensionAttachTo; }>(); collectEntityPageContents(element, { diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index c5138dac3d..f9e8793613 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -156,7 +156,7 @@ describe('collectLegacyRoutes', () => { if={isKind('component')} children={ - + } /> diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 6464032673..1ca118eb88 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -29,6 +29,7 @@ import { ProgressProps, ExternalRouteRef, IconComponent, + IconElement, IconsApi, RouteFunc, RouteRef, @@ -41,7 +42,7 @@ import { NotFoundErrorPage, ErrorDisplay, } from '@backstage/frontend-plugin-api'; -import { ComponentType, useMemo } from 'react'; +import { ComponentType, createElement, useMemo } from 'react'; import { ReactNode } from 'react'; import { toLegacyPlugin } from './BackwardsCompatProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -99,6 +100,11 @@ class CompatIconsApi implements IconsApi { this.#app = app; } + icon(key: string): IconElement | undefined { + const Icon = this.#app.getSystemIcon(key); + return Icon ? createElement(Icon) : undefined; + } + getIcon(key: string): IconComponent | undefined { return this.#app.getSystemIcon(key); } diff --git a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx index 5ded572ca2..f8436e7434 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx +++ b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx @@ -40,11 +40,13 @@ describe('convertLegacyPlugin', () => { "externalRoutes": {}, "featureFlags": [], "getExtension": [Function], + "icon": undefined, "id": "test", "info": [Function], "infoOptions": undefined, "pluginId": "test", "routes": {}, + "title": undefined, "toString": [Function], "version": "v1", "withOverrides": [Function], diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 6d8041ef44..b273fae9e4 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -31,3 +31,4 @@ export { convertLegacyRouteRefs, type ToNewRouteRef, } from './convertLegacyRouteRef'; +export { withApis } from './withApis'; diff --git a/packages/core-compat-api/src/withApis.test.tsx b/packages/core-compat-api/src/withApis.test.tsx new file mode 100644 index 0000000000..eb13ec76ee --- /dev/null +++ b/packages/core-compat-api/src/withApis.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2026 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 { createApiRef } from '@backstage/frontend-plugin-api'; +import { + TestApiProvider, + withLogCollector, +} from '@backstage/frontend-test-utils'; +import { render, screen } from '@testing-library/react'; +import { withApis } from './withApis'; + +describe('withApis', () => { + type MyApi = () => string; + const myApiRef = createApiRef({ id: 'my-api' }); + + const MyComponent = withApis({ getMessage: myApiRef })(({ getMessage }) => { + return

    message: {getMessage()}

    ; + }); + + it('should inject APIs as props and set display name', () => { + render( + 'hello']]}> + + , + ); + + expect(screen.getByText('message: hello')).toBeInTheDocument(); + expect(MyComponent.displayName).toBe('withApis(Component)'); + }); + + it('should ignore properties from the prototype', () => { + const otherRef = createApiRef({ id: 'other' }); + const proto = { other: otherRef }; + const props = { getMessage: { enumerable: true, value: myApiRef } }; + const obj = Object.create(proto, props) as { + getMessage: typeof myApiRef; + other: typeof otherRef; + }; + + const WeirdComponent = withApis(obj)(({ getMessage }) => { + return

    message: {getMessage()}

    ; + }); + + render( + 'hello']]}> + + , + ); + + expect(screen.getByText('message: hello')).toBeInTheDocument(); + }); + + it('should throw NotImplementedError if the API is not available', () => { + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).toThrow('No implementation available for apiRef{my-api}'); + }).error, + ).toEqual( + expect.arrayContaining([ + expect.stringContaining( + 'No implementation available for apiRef{my-api}', + ), + ]), + ); + }); +}); diff --git a/packages/core-compat-api/src/withApis.tsx b/packages/core-compat-api/src/withApis.tsx new file mode 100644 index 0000000000..fc7fc548ee --- /dev/null +++ b/packages/core-compat-api/src/withApis.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2020 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 { ComponentType, PropsWithChildren } from 'react'; +import { TypesToApiRefs, useApiHolder } from '@backstage/frontend-plugin-api'; +import { NotImplementedError } from '@backstage/errors'; + +/** + * Wrapper for giving component an API context. + * + * @param apis - APIs for the context. + * @public + */ +export function withApis(apis: TypesToApiRefs) { + return function withApisWrapper( + WrappedComponent: ComponentType, + ) { + const Hoc = (props: PropsWithChildren>) => { + const apiHolder = useApiHolder(); + + const impls = {} as T; + + for (const key in apis) { + if (Object.hasOwn(apis, key)) { + const ref = apis[key]; + + const api = apiHolder.get(ref); + if (!api) { + throw new NotImplementedError( + `No implementation available for ${ref}`, + ); + } + impls[key] = api; + } + } + + return ; + }; + const displayName = + WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + Hoc.displayName = `withApis(${displayName})`; + + return Hoc; + }; +} diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index b38989ce75..58ebad5095 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/core-components +## 0.18.8-next.1 + +### Patch Changes + +- 8b1a847: Fixed Table component layout when both `filters` and `title` props are used together. The filter controls now use a dedicated CSS class (`filterControls`) instead of incorrectly reusing the root container class. +- 470f72d: The `LogViewer` component from `@backstage/core-components` now supports downloading logs if a callback is passed to `onDownloadLogs` +- Updated dependencies + - @backstage/core-plugin-api@1.12.4-next.1 + +## 0.18.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/version-bridge@1.0.12 + +## 0.18.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- cebfea7: Removed link styles from LinkButton to avoid styling inconsistencies related to import order. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 0.18.7-next.2 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index a0676cde90..ecc27a6cc5 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.7-next.2", + "version": "0.18.8-next.1", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index e6a54e32a4..f373dd56ff 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -21,6 +21,9 @@ export const coreComponentsTranslationRef: TranslationRef< 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'; @@ -44,9 +47,6 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'errorPage.goBack': 'Go back'; readonly 'errorPage.showMoreDetails': 'Show more details'; readonly 'errorPage.showLessDetails': 'Show less details'; - 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 'supportConfig.default.title': 'Support Not Configured'; readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key'; readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; @@ -64,6 +64,7 @@ export const coreComponentsTranslationRef: TranslationRef< 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'; } >; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index 5e842929e6..d487cc00b0 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -512,7 +512,7 @@ export function FeatureCalloutCircular( ): JSX_2.Element; // @public (undocumented) -export type FiltersContainerClassKey = 'root' | 'title'; +export type FiltersContainerClassKey = 'root' | 'filterControls' | 'title'; // @public export function Gauge(props: GaugeProps): JSX_2.Element; @@ -652,15 +652,7 @@ export type HorizontalScrollGridClassKey = export type IconComponentProps = ComponentProps; // @public (undocumented) -export function IconLinkVertical({ - color, - disabled, - href, - icon, - label, - onClick, - title, -}: IconLinkVerticalProps): JSX_2.Element; +export function IconLinkVertical(input: IconLinkVerticalProps): JSX_2.Element; // @public (undocumented) export type IconLinkVerticalClassKey = @@ -827,6 +819,7 @@ export interface LogViewerProps { classes?: { root?: string; }; + onDownloadLog?: () => void; text: string; textWrap?: boolean; } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 26ddecfcfc..d0580d9191 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -27,6 +27,10 @@ const RealLogViewer = lazy(() => * @public */ export interface LogViewerProps { + /** + * Callback function to handle the download log action, and show the download button. + */ + onDownloadLog?: () => void; /** * The text of the logs to display. * diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx index 461e058d2d..30eaee23fc 100644 --- a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -22,10 +22,14 @@ import Typography from '@material-ui/core/Typography'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import FilterListIcon from '@material-ui/icons/FilterList'; +import GetApp from '@material-ui/icons/GetApp'; +import ToolTip from '@material-ui/core/Tooltip'; import { coreComponentsTranslationRef } from '../../translation'; import { LogViewerSearch } from './useLogViewerSearch'; -export interface LogViewerControlsProps extends LogViewerSearch {} +export interface LogViewerControlsProps extends LogViewerSearch { + onDownloadLog?: () => void; +} export function LogViewerControls(props: LogViewerControlsProps) { const { t } = useTranslationRef(coreComponentsTranslationRef); @@ -72,6 +76,13 @@ export function LogViewerControls(props: LogViewerControlsProps) { )} + {Boolean(props?.onDownloadLog) ? ( + + + + + + ) : null} ); } diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx index 928bc77678..3d937cf32c 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -75,4 +75,41 @@ describe('RealLogViewer', () => { expect(copyToClipboard).toHaveBeenCalledWith('Derp'); }); + + it('should render download button when showDownloadButton is true', async () => { + const onDownloadLog = jest.fn(); + const rendered = await renderInTestApp( + , + ); + + const downloadButton = rendered.getByRole('button', { name: /download/i }); + expect(downloadButton).toBeInTheDocument(); + + await userEvent.click(downloadButton); + expect(onDownloadLog).toHaveBeenCalledTimes(1); + }); + + it('should not render download button when showDownloadButton is false', async () => { + const rendered = await renderInTestApp( + , + ); + + const downloadButton = rendered.queryByRole('button', { + name: /download/i, + }); + expect(downloadButton).not.toBeInTheDocument(); + }); + + it('should not render download button by default', async () => { + const rendered = await renderInTestApp(); + + const downloadButton = rendered.queryByRole('button', { + name: /download/i, + }); + expect(downloadButton).not.toBeInTheDocument(); + }); }); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index 1936061b1c..e1e2d71125 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -32,6 +32,8 @@ import { useLogViewerSelection } from './useLogViewerSelection'; import Snackbar from '@material-ui/core/Snackbar'; export interface RealLogViewerProps { + showDownloadButton?: boolean; + onDownloadLog?: () => void; text: string; textWrap?: boolean; classes?: { root?: string }; @@ -187,7 +189,10 @@ export function RealLogViewer(props: RealLogViewerProps) { return ( - + {shouldTextWrap ? ( diff --git a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index 328ab50a34..a34ae21892 100644 --- a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -20,10 +20,11 @@ import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; -import { useState } from 'react'; +import { createElement, isValidElement, useState } from 'react'; import { isError } from '@backstage/errors'; import { configApiRef, + IconComponent, PendingOAuthRequest, useApi, } from '@backstage/core-plugin-api'; @@ -68,7 +69,7 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { } }; - const IconComponent = request.provider.icon; + const providerIcon = request.provider.icon; const message = request.provider.message ?? t('oauthRequestDialog.message', { @@ -76,11 +77,14 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { provider: request.provider.title, }); + const iconElement = + providerIcon === null || isValidElement(providerIcon) + ? providerIcon + : createElement(providerIcon as IconComponent, { fontSize: 'large' }); + return ( - - - + {iconElement ?? <>} ', () => { expect(rendered.getByText('subtitle')).toBeInTheDocument(); }); + it('renders with both title and filters without layout issues', async () => { + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByText('My Table')).toBeInTheDocument(); + expect(rendered.getByText('Filters (0)')).toBeInTheDocument(); + }); + it('renders custom empty component if empty', async () => { const rendered = await renderInTestApp(
    ({ @@ -188,6 +188,10 @@ const useFilterStyles = makeStyles( justifyContent: 'space-between', flexWrap: 'wrap', }, + filterControls: { + display: 'flex', + alignItems: 'center', + }, title: { fontWeight: theme.typography.fontWeightBold, fontSize: 18, @@ -316,7 +320,7 @@ export function TableToolbar(toolbarProps: { if (hasFilters) { return ( - + diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index efe1f97dfb..984488cdbb 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -116,6 +116,17 @@ const makeSidebarStyles = (sidebarConfig: SidebarConfig) => font: 'inherit', textTransform: 'none', }, + itemIcon: { + display: 'inline-flex', + fontSize: theme.typography.fontSize, + lineHeight: 0, + '& svg': { + width: '1.5em', + height: '1.5em', + fontSize: 'inherit', + flexShrink: 0, + }, + }, closed: { width: sidebarConfig.drawerWidthClosed, justifyContent: 'center', @@ -401,7 +412,9 @@ const SidebarItemBase = forwardRef< const displayItemIcon = ( - + + + {!isOpen && hasSubmenu ? : <>} ); diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index 3d0e62747e..1c063195c9 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -128,6 +128,9 @@ export const coreComponentsTranslationRef = createTranslationRef({ 'You do not appear to be signed in. Please try reloading the browser page.', }, logViewer: { + downloadBtn: { + tooltip: 'Download logs', + }, searchField: { placeholder: 'Search', }, diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index cde546e760..5a27761209 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/core-plugin-api +## 1.12.4-next.1 + +### Patch Changes + +- 0452d02: Add optional `description` field to plugin-level feature flags. +- fe848e0: Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## 1.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## 1.12.3 + +### Patch Changes + +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/version-bridge@1.0.12 + ## 1.12.3-next.1 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 8c3c496ebd..8af605aa72 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-plugin-api", - "version": "1.12.3-next.1", + "version": "1.12.4-next.1", "description": "Core API used by Backstage plugins", "backstage": { "role": "web-library" diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index dc5a042488..654bbe94d1 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -500,6 +500,7 @@ export type PluginConfig< // @public export type PluginFeatureFlagConfig = { name: string; + description?: string; }; export { ProfileInfo }; @@ -508,7 +509,7 @@ export { ProfileInfoApi }; // @public export type RouteFunc = ( - ...[params]: Params extends undefined ? readonly [] : readonly [Params] + ...input: Params extends undefined ? readonly [] : readonly [Params] ) => string; // @public diff --git a/packages/core-plugin-api/src/app/useApp.test.tsx b/packages/core-plugin-api/src/app/useApp.test.tsx index 2cee7f6f3c..7888df0464 100644 --- a/packages/core-plugin-api/src/app/useApp.test.tsx +++ b/packages/core-plugin-api/src/app/useApp.test.tsx @@ -50,6 +50,9 @@ describe('useApp', () => { describe('new system', () => { const mockIcon = () => null; const mockIconsApi: IconsApi = { + icon: jest.fn((key: string) => + key === 'test-icon' ? mockIcon() : undefined, + ), getIcon: jest.fn((key: string) => key === 'test-icon' ? mockIcon : undefined, ), diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index 3ba7706e16..057f493530 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -73,6 +73,8 @@ export type BackstagePlugin< export type PluginFeatureFlagConfig = { /** Feature flag name */ name: string; + /** Feature flag description */ + description?: string; }; /** diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 65da38edda..b28a883296 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/create-app +## 0.7.10-next.2 + +### Patch Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.2 + +## 0.7.10-next.1 + +### Patch Changes + +- a9d23c4: Properly support `package.json` `workspaces` field +- ebd4630: Replace deprecated `workspaces.packages` with `workspaces` in `package.json` + + This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + + ```diff + - "workspaces": { + - "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" + - ] + - }, + ], + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + +## 0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## 0.7.9 + +### Patch Changes + +- 40f2720: Updated to include the missing core plugins in the template used with the `--next` flag. Also updated `react-router*` versions and added Jest 30-related dependencies. Finally, moved the order of `@playwright/test` so it won't trigger a file change during the creation process. +- 1ea737c: Bumped create-app version. +- 7c41134: Bumped create-app version. +- 65ba820: Updated the app template sidebar to use the new `NavContentBlueprint` API for page-based navigation. +- 7455dae: Use node prefix on native imports +- c38b74d: Switched `next-app` template to use blueprint from `@backstage/plugin-app-react`. +- Updated dependencies + - @backstage/cli-common@0.1.18 + ## 0.7.9-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a1271c47ee..b39fd01a13 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.9-next.2", + "version": "0.7.10-next.2", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" @@ -45,7 +45,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", - "commander": "^12.0.0", + "commander": "^14.0.3", "fs-extra": "^11.2.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 9d4d5449d1..04395f7cd9 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -22,3 +22,8 @@ version "1.11.31" resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.31.tgz#e5de9ed005551ce9a16aa69e79935fc33065475c" integrity sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A== + +fast-xml-builder@*: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz#a485d7e8381f1db983cf006f849d1066e2935241" + integrity sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ== diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index c854867de6..c38186b1d8 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -19,12 +19,17 @@ import path from 'node:path'; import { Command } from 'commander'; import * as tasks from './lib/tasks'; import createApp from './createApp'; -import { findPaths } from '@backstage/cli-common'; +import { findOwnPaths, targetPaths } from '@backstage/cli-common'; import { tmpdir } from 'node:os'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; jest.mock('./lib/tasks'); +const MOCK_TARGET_DIR = '/mock/target-dir'; +const MOCK_TARGET_ROOT = '/mock/target-root'; +overrideTargetPaths({ dir: MOCK_TARGET_DIR, rootDir: MOCK_TARGET_ROOT }); + // By mocking this the filesystem mocks won't mess with reading all of the package.jsons jest.mock('./lib/versions', () => ({ packageVersions: { root: '1.0.0' }, @@ -64,12 +69,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findPaths(__dirname).resolveTarget( - 'packages', - 'create-app', - 'templates', - 'default-app', - ), + findOwnPaths(__dirname).resolve('templates/default-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), @@ -85,12 +85,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findPaths(__dirname).resolveTarget( - 'packages', - 'create-app', - 'templates', - 'default-app', - ), + findOwnPaths(__dirname).resolve('templates/default-app'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); expect(buildAppMock).toHaveBeenCalled(); @@ -103,12 +98,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findPaths(__dirname).resolveTarget( - 'packages', - 'create-app', - 'templates', - 'next-app', - ), + findOwnPaths(__dirname).resolve('templates/next-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), @@ -127,7 +117,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findPaths(__dirname).resolveTarget('templateDirectory'), + targetPaths.resolve('templateDirectory'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); expect(buildAppMock).toHaveBeenCalled(); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 11e5503899..64db2e52ab 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -18,7 +18,7 @@ import chalk from 'chalk'; import { OptionValues } from 'commander'; import inquirer, { Answers } from 'inquirer'; import { resolve as resolvePath } from 'node:path'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths, findOwnPaths } from '@backstage/cli-common'; import os from 'node:os'; import fs from 'fs-extra'; import { @@ -36,8 +36,6 @@ import { const DEFAULT_BRANCH = 'master'; export default async (opts: OptionValues): Promise => { - /* eslint-disable-next-line no-restricted-syntax */ - const paths = findPaths(__dirname); const answers: Answers = await inquirer.prompt([ { type: 'input', @@ -66,20 +64,22 @@ export default async (opts: OptionValues): Promise => { ]); // Pick the built-in template based on the --next flag + /* eslint-disable-next-line no-restricted-syntax */ + const ownPaths = findOwnPaths(__dirname); const builtInTemplate = opts.next - ? paths.resolveOwn('templates/next-app') - : paths.resolveOwn('templates/default-app'); + ? ownPaths.resolve('templates/next-app') + : ownPaths.resolve('templates/default-app'); // Use `--template-path` argument as template when specified. Otherwise, use the default template. const templateDir = opts.templatePath - ? paths.resolveTarget(opts.templatePath) + ? targetPaths.resolve(opts.templatePath) : builtInTemplate; // Use `--path` argument as application directory when specified, otherwise // create a directory using `answers.name` const appDir = opts.path - ? resolvePath(paths.targetDir, opts.path) - : resolvePath(paths.targetDir, answers.name); + ? resolvePath(targetPaths.dir, opts.path) + : resolvePath(targetPaths.dir, answers.name); Task.log(); Task.log('Creating the app...'); @@ -102,7 +102,7 @@ export default async (opts: OptionValues): Promise => { // Template to temporary location, and then move files Task.section('Checking if the directory is available'); - await checkAppExistsTask(paths.targetDir, answers.name); + await checkAppExistsTask(targetPaths.dir, answers.name); Task.section('Creating a temporary app directory'); const tempDir = await fs.mkdtemp(resolvePath(os.tmpdir(), answers.name)); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index be6852e3ab..6416681b27 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -50,6 +50,7 @@ jest.mock('./versions', () => ({ packageVersions: { root: '1.2.3', '@backstage/cli': '1.0.0', + '@backstage/cli-defaults': '1.0.0', '@backstage/backend-defaults': '1.0.0', '@backstage/backend-tasks': '1.0.0', '@backstage/catalog-model': '1.0.0', diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index db865a2db5..006dd6d7b8 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -38,6 +38,7 @@ import { version as backendDefaults } from '../../../backend-defaults/package.js 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 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'; @@ -107,6 +108,7 @@ export const packageVersions = { '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, + '@backstage/cli-defaults': cliDefaults, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 4e723ea078..4262e0fc82 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,14 +22,13 @@ "prettier:check": "prettier --check .", "new": "backstage-cli new" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-defaults": "^{{version '@backstage/cli-defaults'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@jest/environment-jsdom-abstract": "^30.0.0", "@playwright/test": "^1.32.3", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index f75d984366..367652b70d 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -128,10 +128,10 @@ const overviewContent = ( {entityWarningContent} - + - + @@ -266,7 +266,7 @@ const apiPage = ( - + @@ -298,10 +298,10 @@ const userPage = ( {entityWarningContent} - + - + @@ -314,10 +314,10 @@ const groupPage = ( {entityWarningContent} - + - + @@ -336,10 +336,10 @@ const systemPage = ( {entityWarningContent} - + - + @@ -357,7 +357,6 @@ const systemPage = ( {entityWarningContent} - + - + diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 80ff4a9a06..49436f5fcf 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -44,14 +44,13 @@ } } }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/cli-defaults": "^{{version '@backstage/cli-defaults'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", "@jest/environment-jsdom-abstract": "^30.0.0", "@playwright/test": "^1.32.3", 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 9b7cd7a7e4..d436252edf 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 @@ -1,4 +1,5 @@ import { + Sidebar, SidebarDivider, SidebarGroup, SidebarItem, @@ -6,11 +7,8 @@ import { SidebarSpace, } from '@backstage/core-components'; import { compatWrapper } from '@backstage/core-compat-api'; -import { Sidebar } from '@backstage/core-components'; import { NavContentBlueprint } from '@backstage/plugin-app-react'; import { SidebarLogo } from './SidebarLogo'; -import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import HomeIcon from '@material-ui/icons/Home'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarSearchModal } from '@backstage/plugin-search'; @@ -19,8 +17,15 @@ import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; export const SidebarContent = NavContentBlueprint.make({ params: { - component: ({ items }) => - compatWrapper( + component: ({ navItems }) => { + const nav = navItems.withComponent(item => ( + item.icon} + to={item.href} + text={item.title} + /> + )); + return compatWrapper( } to="/search"> @@ -28,20 +33,11 @@ export const SidebarContent = NavContentBlueprint.make({ }> - {/* Global nav, not org-specific */} - - - {/* End global nav */} + {nav.take('page:catalog')} + {nav.take('page:scaffolder')} - {/* Items in this group will be scrollable if they run out of space */} - {items.map((item, index) => ( - - ))} + {nav.rest({ sortBy: 'title' })} @@ -56,6 +52,7 @@ export const SidebarContent = NavContentBlueprint.make({ , - ), + ); + }, }, }); diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 198b100a69..f00681aa3c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/dev-utils +## 1.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + +## 1.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + +## 1.1.20 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/app-defaults@1.7.5 + ## 1.1.20-next.2 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index c567499e22..7ddfd34b57 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.20-next.2", + "version": "1.1.21-next.1", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test-utils/CHANGELOG.md b/packages/e2e-test-utils/CHANGELOG.md index 47524e8f1f..17c3d443ea 100644 --- a/packages/e2e-test-utils/CHANGELOG.md +++ b/packages/e2e-test-utils/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/e2e-test-utils +## 0.1.2 + +### Patch Changes + +- b96c20e: Added optional `channel` option to `generateProjects()` to allow customizing the Playwright browser channel for testing against different browsers variants. When not provided, the function defaults to 'chrome' to maintain backward compatibility. + + Example usage: + + ```ts + import { generateProjects } from '@backstage/e2e-test-utils'; + + export default defineConfig({ + projects: generateProjects({ channel: 'msedge' }), + }); + ``` + +- 7455dae: Use node prefix on native imports + ## 0.1.2-next.1 ### Patch Changes diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index 834fa31704..edf88711db 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/e2e-test-utils", - "version": "0.1.2-next.1", + "version": "0.1.2", "description": "Shared end-to-end test utilities Backstage", "backstage": { "role": "node-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index badbaf6581..525bc47606 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,22 @@ # e2e-test +## 0.2.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/create-app@0.7.10-next.0 + - @backstage/errors@1.2.7 + +## 0.2.37 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.9 + - @backstage/cli-common@0.1.18 + ## 0.2.37-next.0 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index edc4fbe9d2..782fea0710 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,6 +1,6 @@ { "name": "e2e-test", - "version": "0.2.37-next.0", + "version": "0.2.38-next.0", "description": "E2E test for verifying Backstage packages", "backstage": { "role": "cli" @@ -36,7 +36,7 @@ "@backstage/create-app": "workspace:^", "@backstage/errors": "workspace:^", "chalk": "^4.0.0", - "commander": "^12.0.0", + "commander": "^14.0.3", "cross-fetch": "^4.0.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.3", diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index f0701d0e5d..a86e7f303d 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -27,11 +27,11 @@ import { waitFor, print } from '../lib/helpers'; import mysql from 'mysql2/promise'; import pgtools from 'pgtools'; -import { findPaths, runOutput, run } from '@backstage/cli-common'; import { OptionValues } from 'commander'; +import { findOwnPaths, runOutput, run } from '@backstage/cli-common'; -// eslint-disable-next-line no-restricted-syntax -const paths = findPaths(__dirname); +/* eslint-disable-next-line no-restricted-syntax */ +const ownPaths = findOwnPaths(__dirname); const templatePackagePaths = [ 'packages/cli/templates/frontend-plugin/package.json.hbs', @@ -138,7 +138,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { } for (const pkgJsonPath of templatePackagePaths) { - const jsonPath = paths.resolveOwnRoot(pkgJsonPath); + const jsonPath = ownPaths.resolveRoot(pkgJsonPath); const pkgTemplate = await fs.readFile(jsonPath, 'utf8'); const pkg = JSON.parse( handlebars.compile(pkgTemplate)( @@ -196,7 +196,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { print('Pinning yarn version in workspace'); await pinYarnVersion(workspaceDir); - const yarnPatchesPath = paths.resolveOwnRoot('.yarn/patches'); + const yarnPatchesPath = ownPaths.resolveRoot('.yarn/patches'); if (await fs.pathExists(yarnPatchesPath)) { print('Copying yarn patches'); await fs.copy(yarnPatchesPath, resolvePath(workspaceDir, '.yarn/patches')); @@ -214,7 +214,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { * Pin the yarn version in a directory to the one we're using in the Backstage repo */ async function pinYarnVersion(dir: string) { - const yarnRc = await fs.readFile(paths.resolveOwnRoot('.yarnrc.yml'), 'utf8'); + const yarnRc = await fs.readFile(ownPaths.resolveRoot('.yarnrc.yml'), 'utf8'); const yarnRcLines = yarnRc.split(/\r?\n/); const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarnPath:')); if (!yarnPathLine) { @@ -225,8 +225,8 @@ async function pinYarnVersion(dir: string) { throw new Error(`Invalid 'yarnPath' in ${yarnRc}`); } const [, localYarnPath] = match; - const yarnPath = paths.resolveOwnRoot(localYarnPath); - const yarnPluginPath = paths.resolveOwnRoot( + const yarnPath = ownPaths.resolveRoot(localYarnPath); + const yarnPluginPath = ownPaths.resolveRoot( localYarnPath, '../../plugins/@yarnpkg/plugin-workspace-tools.cjs', ); @@ -328,7 +328,7 @@ async function createApp( */ async function overrideYarnLockSeed(appDir: string) { const content = await fs.readFile( - paths.resolveOwnRoot('packages/create-app/seed-yarn.lock'), + ownPaths.resolveRoot('packages/create-app/seed-yarn.lock'), 'utf8', ); const trimmedContent = content @@ -383,9 +383,13 @@ async function createPlugin(options: { try { let stdout = ''; + let stderr = ''; child.stdout?.on('data', (data: Buffer) => { stdout = stdout + data.toString('utf8'); }); + child.stderr?.on('data', (data: Buffer) => { + stderr = stderr + data.toString('utf8'); + }); print('Waiting for plugin create script to be done'); await child.waitForExit(); diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 3eb1e96932..71b385bcfe 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/eslint-plugin +## 0.2.2-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + +## 0.2.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + ## 0.2.1-next.0 ### Patch Changes diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index e8ee18ed8c..89e08206ce 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/eslint-plugin", - "version": "0.2.1-next.0", + "version": "0.2.2-next.0", "description": "Backstage ESLint plugin", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ }, "dependencies": { "@manypkg/get-packages": "^1.1.3", - "minimatch": "^9.0.0" + "minimatch": "^10.2.1" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/filter-predicates/CHANGELOG.md b/packages/filter-predicates/CHANGELOG.md index e25eb3fc38..3ae443f5b4 100644 --- a/packages/filter-predicates/CHANGELOG.md +++ b/packages/filter-predicates/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/filter-predicates +## 0.1.0 + +### Minor Changes + +- 7feb83b: Introduced package, basically as the extracted predicate types from `@backstage/plugin-catalog-react/alpha` + ## 0.1.0-next.0 ### Minor Changes diff --git a/packages/filter-predicates/package.json b/packages/filter-predicates/package.json index 7f90ab1391..7c2a6bbf80 100644 --- a/packages/filter-predicates/package.json +++ b/packages/filter-predicates/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/filter-predicates", - "version": "0.1.0-next.0", + "version": "0.1.0", "description": "A library for expressing filter predicates and evaluating them against values", "backstage": { "role": "common-library" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index b721fa3d8e..a0c69e5977 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/frontend-app-api +## 0.16.0-next.1 + +### Minor Changes + +- 92af1ae: **BREAKING**: Removed the `allowUnknownExtensionConfig` option from `createSpecializedApp`. This flag had no effect and was a no-op, so no behavioral changes are expected. + +### Patch Changes + +- 0452d02: Add optional `description` field to plugin-level feature flags. +- dab6c46: Added the `ExtensionFactoryMiddleware` type as a public export. +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-defaults@0.5.0-next.1 + +## 0.15.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## 0.15.0 + +### Minor Changes + +- 55b2ef6: **BREAKING**: Updated the behavior of the new API override logic to reject the override and block app startup instead of just logging a deprecation warning. + +### Patch Changes + +- 7edb810: Implemented support for the `internal` extension input option. +- 492503a: Updated error reporting and app tree resolution logic to attribute errors to the correct extension and allow app startup to proceed more optimistically: + + - If an attachment fails to provide the required input data, the error is now attributed to the attachment rather than the parent extension. + - Singleton extension inputs will now only forward attachment errors if the input is required. + - Array extension inputs will now filter out failed attachments instead of failing the entire app tree resolution. + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. +- 9554c36: **DEPRECATED**: Deprecated support for multiple attachment points. +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 09032d7: Internal update to simplify testing utility implementations. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/frontend-defaults@0.4.0 + - @backstage/core-app-api@1.19.5 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 0.15.0-next.2 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index f84b3d1cf4..1951c18828 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.15.0-next.2", + "version": "0.16.0-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index e8fc019007..bf69cc83e6 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -4,10 +4,13 @@ ```ts import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiHolder as ApiHolder_2 } from '@backstage/frontend-plugin-api'; import { AppNode } from '@backstage/frontend-plugin-api'; import { AppTree } from '@backstage/frontend-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; -import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; +import { ExtensionDataContainer } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDataValue } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -169,7 +172,6 @@ export type CreateSpecializedAppOptions = { bindRoutes?(context: { bind: CreateAppRouteBinder }): void; advanced?: { apis?: ApiHolder; - allowUnknownExtensionConfig?: boolean; extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; @@ -177,6 +179,18 @@ export type CreateSpecializedAppOptions = { }; }; +// @public (undocumented) +export type ExtensionFactoryMiddleware = ( + originalFactory: (contextOverrides?: { + config?: JsonObject; + }) => ExtensionDataContainer, + context: { + node: AppNode; + apis: ApiHolder_2; + config?: JsonObject; + }, +) => Iterable>; + // @public export type FrontendPluginInfoResolver = (ctx: { packageJson(): Promise; diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts new file mode 100644 index 0000000000..558c160ecb --- /dev/null +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright 2026 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 { createElement, memo, forwardRef } from 'react'; +import { DefaultIconsApi } from './DefaultIconsApi'; + +describe('DefaultIconsApi', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return undefined for unknown keys', () => { + const api = new DefaultIconsApi({}); + expect(api.icon('missing')).toBeUndefined(); + expect(api.getIcon('missing')).toBeUndefined(); + }); + + it('should list all registered icon keys', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + const api = new DefaultIconsApi({ + a: createElement('span'), + b: () => createElement('span'), + c: null, + }); + expect(api.listIconKeys()).toEqual(['a', 'b', 'c']); + }); + + it('should return IconElement values directly via icon()', () => { + const element = createElement('span', null, 'test-icon'); + const api = new DefaultIconsApi({ myIcon: element }); + + expect(api.icon('myIcon')).toBe(element); + }); + + it('should return null IconElement values via icon()', () => { + const api = new DefaultIconsApi({ empty: null }); + expect(api.icon('empty')).toBeNull(); + }); + + it('should convert IconComponent values to elements for icon()', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + const MyIcon = () => createElement('span', null, 'rendered'); + const api = new DefaultIconsApi({ myIcon: MyIcon }); + + const result = api.icon('myIcon'); + expect(result).toBeTruthy(); + // @ts-expect-error accessing internal React element structure + expect(result.type).toBe(MyIcon); + // @ts-expect-error accessing internal React element structure + expect(result.props.fontSize).toBe('inherit'); + }); + + it('should wrap IconElement values in a component for getIcon()', () => { + const element = createElement('span', null, 'test-icon'); + const api = new DefaultIconsApi({ myIcon: element }); + + const icon = api.getIcon('myIcon'); + expect(icon).toBeDefined(); + expect(typeof icon).toBe('function'); + // @ts-expect-error testing runtime behavior + const result = icon({}); + expect(result.type).toBe('span'); + expect(result.props.style).toEqual({ fontSize: '1.5rem' }); + expect(result.props.children).toBe(element.props.children); + expect(api.getIcon('myIcon')).toBe(icon); + }); + + it('should honor fontSize for getIcon()', () => { + const element = createElement('svg'); + const api = new DefaultIconsApi({ myIcon: element }); + const icon = api.getIcon('myIcon'); + + // @ts-expect-error testing runtime behavior + const result = icon({ fontSize: 'small' }); + expect(result.props.style.fontSize).toBe('1.25rem'); + }); + + it('should forward runtime props to the original icon element', () => { + const element = createElement('svg', { + className: 'existing', + style: { color: 'red' }, + }); + const api = new DefaultIconsApi({ myIcon: element }); + const icon = api.getIcon('myIcon'); + + // @ts-expect-error testing runtime behavior + const result = icon({ className: 'extra', style: { width: '2em' } }); + + expect(result.type).toBe('svg'); + expect(result.props.className).toBe('existing extra'); + expect(result.props.style).toEqual({ + color: 'red', + fontSize: '1.5rem', + width: '2em', + }); + }); + + it('should wrap null IconElement in a component for getIcon()', () => { + const api = new DefaultIconsApi({ empty: null }); + + const icon = api.getIcon('empty'); + expect(icon).toBeDefined(); + expect(typeof icon).toBe('function'); + // @ts-expect-error testing runtime behavior + expect(icon({})).toBeNull(); + }); + + it('should log a single warning listing all IconComponent keys', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + void new DefaultIconsApi({ + a: () => createElement('span'), + elem: createElement('span'), + b: () => createElement('span'), + empty: null, + }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/a, b$/)); + }); + + it('should not warn when only IconElement values are provided', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + void new DefaultIconsApi({ + element: createElement('span'), + empty: null, + }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should treat React.memo components as IconComponent', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const MemoIcon = memo(() => createElement('svg')); + const api = new DefaultIconsApi({ myIcon: MemoIcon }); + + const el = api.icon('myIcon'); + expect(el).toBeTruthy(); + // @ts-expect-error accessing internal React element structure + expect(el.type).toBe(MemoIcon); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('myIcon')); + }); + + it('should treat React.forwardRef components as IconComponent', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const RefIcon = forwardRef(() => createElement('svg')); + // @ts-expect-error forwardRef is not strictly IconComponent but should be handled + const api = new DefaultIconsApi({ myIcon: RefIcon }); + + const el = api.icon('myIcon'); + expect(el).toBeTruthy(); + // @ts-expect-error accessing internal React element structure + expect(el.type).toBe(RefIcon); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('myIcon')); + }); +}); diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index 53a7fa6421..52ac07551a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -14,7 +14,27 @@ * limitations under the License. */ -import { IconComponent, IconsApi } from '@backstage/frontend-plugin-api'; +import { + IconComponent, + IconElement, + IconsApi, +} from '@backstage/frontend-plugin-api'; +import { cloneElement, createElement, isValidElement } from 'react'; + +const legacyFontSizeMap = { + inherit: 'inherit', + small: '1.25rem', + medium: '1.5rem', + large: '2.1875rem', +} as const; + +function mergeClassNames(...classNames: Array) { + const merged = classNames.filter(Boolean).join(' '); + if (merged) { + return merged; + } + return undefined; +} /** * Implementation for the {@link IconsApi} @@ -22,14 +42,80 @@ import { IconComponent, IconsApi } from '@backstage/frontend-plugin-api'; * @internal */ export class DefaultIconsApi implements IconsApi { - #icons: Map; + #icons: Map; + #components = new Map(); - constructor(icons: { [key in string]: IconComponent }) { - this.#icons = new Map(Object.entries(icons)); + constructor(icons: { [key in string]: IconComponent | IconElement }) { + const deprecatedKeys: string[] = []; + + this.#icons = new Map( + Object.entries(icons).map(([key, value]) => { + if (value === null || isValidElement(value)) { + return [key, value]; + } + deprecatedKeys.push(key); + return [ + key, + createElement(value as IconComponent, { fontSize: 'inherit' }), + ]; + }), + ); + + if (deprecatedKeys.length > 0) { + const keys = deprecatedKeys.join(', '); + // eslint-disable-next-line no-console + console.warn( + `The following icons were registered as IconComponent, which is deprecated. Use IconElement instead by passing rather than MyIcon: ${keys}`, + ); + } + } + + icon(key: string): IconElement | undefined { + return this.#icons.get(key); } getIcon(key: string): IconComponent | undefined { - return this.#icons.get(key); + let component = this.#components.get(key); + if (component) { + return component; + } + const el = this.#icons.get(key); + if (el === undefined) { + return undefined; + } + component = props => { + if (el === null) { + return null; + } + + const { + fontSize = 'medium', + className, + style, + ...rest + } = props as { + fontSize?: keyof typeof legacyFontSizeMap; + className?: string; + style?: Record; + } & Record; + + const elementProps = el.props as { + className?: string; + style?: Record; + }; + + return cloneElement(el, { + ...rest, + className: mergeClassNames(elementProps.className, className), + style: { + ...elementProps.style, + fontSize: legacyFontSizeMap[fontSize], + ...style, + }, + }); + }; + this.#components.set(key, component); + return component; } listIconKeys(): string[] { diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index b1da35313e..e52bbfa320 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -17,18 +17,17 @@ import { AppNode, Extension, + ExtensionDataContainer, ExtensionDataRef, ExtensionDefinition, - ExtensionFactoryMiddleware, - ExtensionInput, PortableSchema, - ResolvedExtensionInput, createExtension, createExtensionBlueprint, createExtensionDataRef, createExtensionInput, createFrontendPlugin, } from '@backstage/frontend-plugin-api'; +import { ExtensionFactoryMiddleware } from '../wiring/types'; import { createAppNodeInstance, instantiateAppNodeTree, @@ -146,8 +145,8 @@ function mirrorInputs(ctx: { inputs: { [name in string]: | undefined - | ResolvedExtensionInput - | Array>; + | ({ node: AppNode } & ExtensionDataContainer) + | Array<{ node: AppNode } & ExtensionDataContainer>; }; }) { return [ diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 070cf1d687..9e8e3cc2eb 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -18,10 +18,10 @@ import { ApiHolder, ExtensionDataContainer, ExtensionDataRef, - ExtensionFactoryMiddleware, ExtensionInput, ResolvedExtensionInputs, } from '@backstage/frontend-plugin-api'; +import { ExtensionFactoryMiddleware } from '../wiring/types'; import mapValues from 'lodash/mapValues'; import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index bffc156509..a8e63f82e0 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -149,7 +149,7 @@ describe('buildAppTree', () => { attachTo: [ { id: 'a', input: 'x' }, { id: 'b', input: 'x' }, - ], + ] as any, }, { ...baseSpec, @@ -157,7 +157,7 @@ describe('buildAppTree', () => { attachTo: [ { id: 'b', input: 'x' }, { id: 'c', input: 'x' }, - ], + ] as any, }, ], collector, diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index e21befc80e..51c31aca25 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -103,7 +103,10 @@ describe('createSpecializedApp', () => { features: [ createFrontendPlugin({ pluginId: 'test', - featureFlags: [{ name: 'a' }, { name: 'b' }], + featureFlags: [ + { name: 'a' }, + { name: 'b', description: 'Feature B description' }, + ], extensions: [ createExtension({ attachTo: { id: 'root', input: 'app' }, @@ -146,6 +149,11 @@ describe('createSpecializedApp', () => { expect(screen.getByText('flags:test=a,test=b')).toBeInTheDocument(); + expect(flags).toEqual([ + { name: 'a', pluginId: 'test' }, + { name: 'b', pluginId: 'test', description: 'Feature B description' }, + ]); + expect(app.apis).toMatchInlineSnapshot(` ApiResolver { "apis": Map { diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index d56a609bb9..fda00b70b1 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -29,9 +29,9 @@ import { createApiFactory, routeResolutionApiRef, AppNode, - ExtensionFactoryMiddleware, FrontendFeature, } from '@backstage/frontend-plugin-api'; +import { ExtensionFactoryMiddleware } from './types'; import { AnyApiFactory, ApiHolder, @@ -255,17 +255,6 @@ export type CreateSpecializedAppOptions = { */ apis?: ApiHolder; - /** - * If set to true, the system will silently accept and move on if - * encountering config for extensions that do not exist. The default is to - * reject such config to help catch simple mistakes. - * - * This flag can be useful in some scenarios where you have a dynamic set of - * extensions enabled at different times, but also increases the risk of - * accidentally missing e.g. simple typos in your config. - */ - allowUnknownExtensionConfig?: boolean; - /** * Applies one or more middleware on every extension, as they are added to * the application. @@ -357,6 +346,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { OpaqueFrontendPlugin.toInternal(feature).featureFlags.forEach(flag => featureFlagApi.registerFlag({ name: flag.name, + description: flag.description, pluginId: feature.id, }), ); @@ -365,6 +355,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { toInternalFrontendModule(feature).featureFlags.forEach(flag => featureFlagApi.registerFlag({ name: flag.name, + description: flag.description, pluginId: feature.pluginId, }), ); diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index 1e18859ddd..cae2117df5 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -20,3 +20,4 @@ export { } from './createSpecializedApp'; export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher'; export { type AppError, type AppErrorTypes } from './createErrorCollector'; +export { type ExtensionFactoryMiddleware } from './types'; diff --git a/packages/frontend-app-api/src/wiring/types.ts b/packages/frontend-app-api/src/wiring/types.ts new file mode 100644 index 0000000000..0bb7497179 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { JsonObject } from '@backstage/types'; +import { + ApiHolder, + AppNode, + ExtensionDataContainer, + ExtensionDataRef, + ExtensionDataValue, +} from '@backstage/frontend-plugin-api'; + +/** @public */ +export type ExtensionFactoryMiddleware = ( + originalFactory: (contextOverrides?: { + config?: JsonObject; + }) => ExtensionDataContainer, + context: { + node: AppNode; + apis: ApiHolder; + config?: JsonObject; + }, +) => Iterable>; diff --git a/packages/frontend-defaults/CHANGELOG.md b/packages/frontend-defaults/CHANGELOG.md index 751b492c0c..030d3cace9 100644 --- a/packages/frontend-defaults/CHANGELOG.md +++ b/packages/frontend-defaults/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/frontend-defaults +## 0.5.0-next.1 + +### Minor Changes + +- 92af1ae: **BREAKING**: Removed the `allowUnknownExtensionConfig` option from `createApp`. This flag had no effect and was a no-op, so no behavioral changes are expected. +- 33de79d: **BREAKING**: Removed the deprecated `createPublicSignInApp` function. Use `createApp` from `@backstage/frontend-defaults` with `appModulePublicSignIn` from `@backstage/plugin-app/alpha` instead. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app@0.4.1-next.2 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-app-api@0.15.1-next.0 + +## 0.4.0 + +### Minor Changes + +- 55b2ef6: **BREAKING**: The `API_FACTORY_CONFLICT` warning is now treated as an error and will prevent the app from starting. + +### Patch Changes + +- 122d39c: Completely removed support for the deprecated `app.experimental.packages` configuration. Replace existing usage directly with `app.packages`. +- c38b74d: Dependency update for tests. +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/frontend-app-api@0.15.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app@0.4.0 + ## 0.4.0-next.2 ### Patch Changes diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index aa445ef8e1..0ff81a490f 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-defaults", - "version": "0.4.0-next.2", + "version": "0.5.0-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index 75392d2dc1..72bd0d785e 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -8,7 +8,7 @@ import { AppErrorTypes } from '@backstage/frontend-app-api'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/frontend-plugin-api'; import { CreateAppRouteBinder } from '@backstage/frontend-app-api'; -import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; +import { ExtensionFactoryMiddleware } from '@backstage/frontend-app-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api'; import { FrontendPluginInfoResolver } from '@backstage/frontend-app-api'; @@ -23,7 +23,6 @@ export function createApp(options?: CreateAppOptions): { // @public export interface CreateAppOptions { advanced?: { - allowUnknownExtensionConfig?: boolean; configLoader?: () => Promise<{ config: ConfigApi; }>; @@ -37,11 +36,6 @@ export interface CreateAppOptions { features?: (FrontendFeature | FrontendFeatureLoader)[]; } -// @public @deprecated (undocumented) -export function createPublicSignInApp(options?: CreateAppOptions): { - createRoot(): JSX_2; -}; - // @public (undocumented) export function discoverAvailableFeatures(config: Config): { features: (FrontendFeature | FrontendFeatureLoader)[]; diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 832dd1b0d3..233f0b5460 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -283,7 +283,9 @@ describe('createApp', () => { ).resolves.toBeInTheDocument(); }); - it('should allow unknown extension config if the flag is set', async () => { + it('should warn about unknown extension config', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createApp({ features: [ appPlugin, @@ -300,7 +302,6 @@ describe('createApp', () => { }), ], advanced: { - allowUnknownExtensionConfig: true, configLoader: async () => ({ config: mockApis.config({ data: { @@ -316,6 +317,12 @@ describe('createApp', () => { await renderWithEffects(app.createRoot()); await expect(screen.findByText('Derp')).resolves.toBeInTheDocument(); + expect(warnSpy).toHaveBeenCalledWith('App startup encountered warnings:'); + expect(warnSpy).toHaveBeenCalledWith( + 'INVALID_EXTENSION_CONFIG_KEY: Extension unknown:lols/wut does not exist', + ); + + warnSpy.mockRestore(); }); it('should make the app structure available through the AppTreeApi', async () => { let appTreeApi: AppTreeApi | undefined = undefined; @@ -390,11 +397,13 @@ describe('createApp', () => { + ] + ] diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 4ff67f6d5a..220a7b743f 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -18,7 +18,6 @@ import { JSX, lazy, ReactNode, Suspense } from 'react'; import { ConfigApi, coreExtensionData, - ExtensionFactoryMiddleware, FrontendFeature, FrontendFeatureLoader, } from '@backstage/frontend-plugin-api'; @@ -31,6 +30,7 @@ import { ConfigReader } from '@backstage/config'; import { CreateAppRouteBinder, createSpecializedApp, + ExtensionFactoryMiddleware, FrontendPluginInfoResolver, } from '@backstage/frontend-app-api'; import appPlugin from '@backstage/plugin-app'; @@ -58,17 +58,6 @@ export interface CreateAppOptions { * Advanced, more rarely used options. */ advanced?: { - /** - * If set to true, the system will silently accept and move on if - * encountering config for extensions that do not exist. The default is to - * reject such config to help catch simple mistakes. - * - * This flag can be useful in some scenarios where you have a dynamic set of - * extensions enabled at different times, but also increases the risk of - * accidentally missing e.g. simple typos in your config. - */ - allowUnknownExtensionConfig?: boolean; - /** * Sets a custom config loader, replacing the builtin one. * diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx deleted file mode 100644 index 06a0a62271..0000000000 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2024 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 { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { useEffect } from 'react'; -import { createPublicSignInApp } from './createPublicSignInApp'; -import { mockApis } from '@backstage/test-utils'; - -describe('createPublicSignInApp', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should render a sign-in page', async () => { - const app = createPublicSignInApp({ - advanced: { - configLoader: async () => ({ config: mockApis.config() }), - }, - features: [ - createFrontendModule({ - pluginId: 'app', - extensions: [ - SignInPageBlueprint.make({ - params: { - loader: async () => () =>
    Sign in page
    , - }, - }), - ], - }), - ], - }); - - render(app.createRoot()); - - await expect( - screen.findByText('Sign in page'), - ).resolves.toBeInTheDocument(); - }); - - it('should render the form redirect on sign-in', async () => { - const submitSpy = jest - .spyOn(HTMLFormElement.prototype, 'submit') - .mockReturnValue(); - - const app = createPublicSignInApp({ - advanced: { - configLoader: async () => ({ config: mockApis.config() }), - }, - features: [ - createFrontendModule({ - pluginId: 'app', - extensions: [ - SignInPageBlueprint.make({ - params: { - loader: - async () => - ({ onSignInSuccess }) => { - useEffect(() => { - onSignInSuccess( - mockApis.identity({ token: 'mock-token' }), - ); - }, [onSignInSuccess]); - return
    ; - }, - }, - }), - ], - }), - ], - }); - - const { baseElement } = render(app.createRoot()); - - await waitFor(() => { - expect(submitSpy).toHaveBeenCalled(); - }); - - expect(baseElement).toMatchInlineSnapshot(` - -
    -
    -
    - - - - -
    - - `); - }); -}); diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index 37ec62e794..c7f3e468a2 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -21,7 +21,6 @@ */ export { createApp, type CreateAppOptions } from './createApp'; -export { createPublicSignInApp } from './createPublicSignInApp'; export { discoverAvailableFeatures } from './discovery'; export { resolveAsyncFeatures } from './resolution'; export { maybeCreateErrorPage } from './maybeCreateErrorPage'; diff --git a/packages/frontend-dev-utils/.eslintrc.js b/packages/frontend-dev-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-dev-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-dev-utils/README.md b/packages/frontend-dev-utils/README.md new file mode 100644 index 0000000000..5a590c1d58 --- /dev/null +++ b/packages/frontend-dev-utils/README.md @@ -0,0 +1,19 @@ +# @backstage/frontend-dev-utils + +Utilities for developing Backstage frontend plugins using the new frontend system. + +This package provides a minimal helper for wiring up a development app from a `dev/` entry point, making it easy to run and test frontend plugins in isolation. + +## Installation + +Install the package via Yarn: + +```sh +cd plugins/ # if within a monorepo +yarn add -D @backstage/frontend-dev-utils +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/frontend-dev-utils/catalog-info.yaml b/packages/frontend-dev-utils/catalog-info.yaml new file mode 100644 index 0000000000..b05c739594 --- /dev/null +++ b/packages/frontend-dev-utils/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-frontend-dev-utils + title: '@backstage/frontend-dev-utils' + description: Utilities for developing Backstage frontend plugins using the new frontend system. +spec: + lifecycle: experimental + type: backstage-web-library + owner: framework-maintainers diff --git a/packages/frontend-dev-utils/knip-report.md b/packages/frontend-dev-utils/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/packages/frontend-dev-utils/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/packages/frontend-dev-utils/package.json b/packages/frontend-dev-utils/package.json new file mode 100644 index 0000000000..33eadabbe2 --- /dev/null +++ b/packages/frontend-dev-utils/package.json @@ -0,0 +1,65 @@ +{ + "name": "@backstage/frontend-dev-utils", + "version": "0.0.0", + "description": "Utilities for developing Backstage frontend plugins using the new frontend system.", + "backstage": { + "role": "web-library" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "keywords": [ + "backstage" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/frontend-dev-utils" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/frontend-defaults": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-app": "workspace:^", + "@backstage/ui": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^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 + } + } +} diff --git a/packages/frontend-dev-utils/report.api.md b/packages/frontend-dev-utils/report.api.md new file mode 100644 index 0000000000..39ca2aec2b --- /dev/null +++ b/packages/frontend-dev-utils/report.api.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/frontend-dev-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CreateAppOptions } from '@backstage/frontend-defaults'; +import { FrontendFeature } from '@backstage/frontend-plugin-api'; +import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api'; + +// @public +export function createDevApp(options: CreateDevAppOptions): void; + +// @public +export interface CreateDevAppOptions { + bindRoutes?: CreateAppOptions['bindRoutes']; + features: (FrontendFeature | FrontendFeatureLoader)[]; +} +``` diff --git a/packages/frontend-dev-utils/src/BuiCss.tsx b/packages/frontend-dev-utils/src/BuiCss.tsx new file mode 100644 index 0000000000..d5ece19b71 --- /dev/null +++ b/packages/frontend-dev-utils/src/BuiCss.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2026 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. + */ + +// This ensures that dev apps always have the BUI CSS loaded. +// eslint-disable-next-line @backstage/no-ui-css-imports-in-non-frontend +import '@backstage/ui/css/styles.css'; + +/** + * Placeholder component to allow lazy loading of the BUI CSS import. This + * ensures that we don't load the CSS as soon as anyone imports this package. + */ +export default () => null; diff --git a/packages/frontend-dev-utils/src/createDevApp.test.tsx b/packages/frontend-dev-utils/src/createDevApp.test.tsx new file mode 100644 index 0000000000..5c0b21ab25 --- /dev/null +++ b/packages/frontend-dev-utils/src/createDevApp.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright 2026 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 { + PageBlueprint, + createFrontendPlugin, +} from '@backstage/frontend-plugin-api'; +import { waitFor, within } from '@testing-library/react'; +import { createDevApp } from './createDevApp'; + +jest.setTimeout(15000); + +const originalEnv = process.env; + +function loadCreateDevAppIsolated(): typeof import('./createDevApp').createDevApp { + let isolatedCreateDevApp: + | typeof import('./createDevApp').createDevApp + | undefined; + + jest.isolateModules(() => { + ({ createDevApp: isolatedCreateDevApp } = require('./createDevApp')); + }); + + if (!isolatedCreateDevApp) { + throw new Error('Expected createDevApp to be loaded in isolation'); + } + + return isolatedCreateDevApp; +} + +describe('createDevApp', () => { + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + document.body.innerHTML = ''; + jest.resetAllMocks(); + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('should render a dev app with a plugin', async () => { + const root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + + const testPlugin = createFrontendPlugin({ + pluginId: 'test', + extensions: [ + PageBlueprint.make({ + params: { + path: '/', + loader: async () =>
    Test Plugin Page
    , + }, + }), + ], + }); + + (process.env as any).APP_CONFIG = [ + { + context: 'test', + data: { + app: { title: 'Test App' }, + backend: { baseUrl: 'http://localhost' }, + }, + }, + ]; + + createDevApp({ + features: [testPlugin], + }); + + const body = within(document.body); + await body.findByText('Test Plugin Page', {}, { timeout: 10000 }); + }); + + it('should forward bindRoutes to createApp', async () => { + jest.resetModules(); + + const bindRoutes = jest.fn(); + const createApp = jest.fn(() => ({ + createRoot: () =>
    Test App Root
    , + })); + const render = jest.fn(); + const createRoot = jest.fn(() => ({ render })); + + jest.doMock('@backstage/frontend-defaults', () => ({ + createApp, + })); + jest.doMock('@backstage/plugin-app', () => ({ + __esModule: true, + default: { + withOverrides: jest.fn(() => 'app-plugin-override'), + getExtension: jest.fn(() => ({ + override: jest.fn(() => 'disabled-sign-in-page'), + })), + }, + })); + jest.doMock('react-dom/client', () => ({ + __esModule: true, + createRoot, + })); + + const root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + + const isolatedCreateDevApp = loadCreateDevAppIsolated(); + isolatedCreateDevApp({ + bindRoutes, + features: ['plugin-feature'] as any, + }); + + await waitFor(() => { + expect(createApp).toHaveBeenCalledWith({ + bindRoutes, + features: ['app-plugin-override', 'plugin-feature'], + }); + expect(createRoot).toHaveBeenCalledWith(root); + }); + + const renderedNode = render.mock.calls[0][0] as any; + expect(renderedNode.props.children).toHaveLength(2); + expect(renderedNode.props.children[0].props.fallback).toBeNull(); + expect(renderedNode.props.children[1].props.children).toBe('Test App Root'); + }); + + it('should throw a clear error when the root element is missing', () => { + expect(() => createDevApp({ features: [] })).toThrow( + "Could not find the dev app root element '#root'; make sure your dev entry HTML contains a root element with that id.", + ); + }); + + it('should fall back to legacy react-dom rendering when createRoot is unavailable', async () => { + jest.resetModules(); + delete process.env.HAS_REACT_DOM_CLIENT; + + const createApp = jest.fn(() => ({ + createRoot: () =>
    Test App Root
    , + })); + const render = jest.fn(); + + jest.doMock('@backstage/frontend-defaults', () => ({ + createApp, + })); + jest.doMock('@backstage/plugin-app', () => ({ + __esModule: true, + default: { + withOverrides: jest.fn(() => 'app-plugin-override'), + getExtension: jest.fn(() => ({ + override: jest.fn(() => 'disabled-sign-in-page'), + })), + }, + })); + jest.doMock('react-dom', () => ({ + __esModule: true, + render, + })); + + const root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + + const isolatedCreateDevApp = loadCreateDevAppIsolated(); + isolatedCreateDevApp({ + features: ['plugin-feature'] as any, + }); + + await waitFor(() => { + expect(render).toHaveBeenCalled(); + expect(createApp).toHaveBeenCalledWith({ + bindRoutes: undefined, + features: ['app-plugin-override', 'plugin-feature'], + }); + }); + }); +}); diff --git a/packages/frontend-dev-utils/src/createDevApp.tsx b/packages/frontend-dev-utils/src/createDevApp.tsx new file mode 100644 index 0000000000..80cc95dc22 --- /dev/null +++ b/packages/frontend-dev-utils/src/createDevApp.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2026 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 { + FrontendFeature, + FrontendFeatureLoader, +} from '@backstage/frontend-plugin-api'; +import { createApp, CreateAppOptions } from '@backstage/frontend-defaults'; +import appPlugin from '@backstage/plugin-app'; +import { Suspense, lazy } from 'react'; +import 'react-dom'; + +type AppPluginWithSimpleOverrides = { + withOverrides(options: { extensions: unknown[] }): FrontendFeature; +}; + +// Collapse the deeply nested override types to avoid excessive instantiation. +const appPluginOverride = ( + appPlugin as unknown as AppPluginWithSimpleOverrides +).withOverrides({ + extensions: [ + appPlugin.getExtension('sign-in-page:app').override({ + disabled: true, + }), + ], +}); + +const BuiCss = lazy(() => import('./BuiCss')); + +let ReactDOMPromise: Promise< + typeof import('react-dom') | typeof import('react-dom/client') +>; +if (process.env.HAS_REACT_DOM_CLIENT) { + ReactDOMPromise = import('react-dom/client'); +} else { + ReactDOMPromise = import('react-dom'); +} + +/** + * Options for {@link createDevApp}. + * + * @public + */ +export interface CreateDevAppOptions { + /** + * The list of features to load in the dev app. + */ + features: (FrontendFeature | FrontendFeatureLoader)[]; + + /** + * Allows for the binding of plugins' external route refs within the dev app. + */ + bindRoutes?: CreateAppOptions['bindRoutes']; +} + +function getRootElement(): HTMLElement { + const rootElement = document.getElementById('root'); + + if (!rootElement) { + throw new Error( + "Could not find the dev app root element '#root'; make sure your dev entry HTML contains a root element with that id.", + ); + } + + return rootElement; +} + +/** + * Creates and renders a minimal development app for the new frontend system. + * + * @example + * ```tsx + * // dev/index.ts + * import { createDevApp } from '@backstage/frontend-dev-utils'; + * import myPlugin from '../src'; + * + * createDevApp({ features: [myPlugin] }); + * ``` + * + * @public + */ +export function createDevApp(options: CreateDevAppOptions): void { + const rootElement = getRootElement(); + const { features, bindRoutes } = options; + const devFeatures: CreateAppOptions['features'] = [ + appPluginOverride, + ...features, + ]; + const appOptions: CreateAppOptions = { + bindRoutes, + features: devFeatures, + }; + const app = createApp(appOptions); + const AppRoot = app.createRoot(); + + ReactDOMPromise.then(ReactDOM => { + const rootNode = ( + <> + + + + {AppRoot} + + ); + + if ('createRoot' in ReactDOM) { + ReactDOM.createRoot(rootElement).render(rootNode); + } else { + ReactDOM.render(rootNode, rootElement); + } + }); +} diff --git a/plugins/scaffolder-backend-module-bitbucket/src/index.ts b/packages/frontend-dev-utils/src/index.ts similarity index 74% rename from plugins/scaffolder-backend-module-bitbucket/src/index.ts rename to packages/frontend-dev-utils/src/index.ts index 7f68a3f395..d93fa3d473 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/index.ts +++ b/packages/frontend-dev-utils/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2026 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. @@ -15,10 +15,9 @@ */ /** - * A module for the scaffolder backend that lets you interact with bitbucket + * Utilities for developing Backstage frontend plugins using the new frontend system. * * @packageDocumentation */ -export * from './deprecated'; -export { bitbucketModule as default } from './module'; +export { createDevApp, type CreateDevAppOptions } from './createDevApp'; diff --git a/packages/frontend-dynamic-feature-loader/CHANGELOG.md b/packages/frontend-dynamic-feature-loader/CHANGELOG.md index d102c57b06..efcc652f5d 100644 --- a/packages/frontend-dynamic-feature-loader/CHANGELOG.md +++ b/packages/frontend-dynamic-feature-loader/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/frontend-dynamic-feature-loader +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/config@1.3.6 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/module-federation-common@0.1.0 + +## 0.1.9 + +### Patch Changes + +- fdbd404: Updated module federation integration to use `@module-federation/enhanced/runtime` `createInstance` API and the new `loadModuleFederationHostShared` from `@backstage/module-federation-common` for loading shared dependencies. Also added support for passing a pre-created `ModuleFederation` instance via the `moduleFederation.instance` option. +- fdbd404: Updated `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` dependencies from `^0.9.0` to `^0.21.6`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/module-federation-common@0.1.0 + ## 0.1.9-next.1 ### Patch Changes diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json index 105e3c0884..b16e984934 100644 --- a/packages/frontend-dynamic-feature-loader/package.json +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-dynamic-feature-loader", - "version": "0.1.9-next.1", + "version": "0.1.10-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx index cb066e8b21..081b3c730a 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -165,7 +165,7 @@ describe('dynamicFrontendFeaturesLoader', () => { shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, { - name: '@mui/material/styles/', + name: '@mui/material/styles', version: '5.16.14', lib: async () => ({ default: {} }), shareConfig: { singleton: true, requiredVersion: '*', eager: true }, diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts index 8623dc3c33..9802afba0f 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,6 +17,7 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** + import { DiscoveryApi } from '../types/discovery'; import { FetchApi } from '../types/fetch'; import crossFetch from 'cross-fetch'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts index fc7c83b736..c2e931caf8 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts index dc3055033d..19501a5dcb 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts index 7440110ad8..ff5cae705f 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts index 72edba90d4..6e8bdeff0a 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts index 0c54a6585b..4cf296a0b9 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts index eaea330f99..465ea2d806 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/packages/frontend-internal/CHANGELOG.md b/packages/frontend-internal/CHANGELOG.md index 654687a36e..4567e87d39 100644 --- a/packages/frontend-internal/CHANGELOG.md +++ b/packages/frontend-internal/CHANGELOG.md @@ -1,5 +1,29 @@ # @internal/frontend +## 0.0.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + +## 0.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## 0.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/version-bridge@1.0.12 + ## 0.0.17-next.1 ### Patch Changes diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json index 92a344d524..52cdb33641 100644 --- a/packages/frontend-internal/package.json +++ b/packages/frontend-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/frontend", - "version": "0.0.17-next.1", + "version": "0.0.18-next.1", "backstage": { "role": "web-library", "inline": true diff --git a/packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts b/packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts index 3e964af5ca..4564be2bb1 100644 --- a/packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts +++ b/packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts @@ -17,6 +17,7 @@ import { Extension, FeatureFlagConfig, + IconElement, OverridableFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; @@ -26,6 +27,8 @@ export const OpaqueFrontendPlugin = OpaqueType.create<{ public: OverridableFrontendPlugin; versions: { readonly version: 'v1'; + readonly title?: string; + readonly icon?: IconElement; readonly extensions: Extension[]; readonly featureFlags: FeatureFlagConfig[]; readonly infoOptions?: { diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 30effaf5c3..3b150b0707 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/frontend-plugin-api +## 0.15.0-next.1 + +### Minor Changes + +- 6573901: **BREAKING**: Removed the deprecated `AnyExtensionDataRef` type. Use `ExtensionDataRef` without type parameters instead. +- a9440f0: **BREAKING**: Simplified the `ExtensionAttachTo` type to only support a single attachment target. The array form for attaching to multiple extension points has been removed. Also removed the deprecated `ExtensionAttachToSpec` type alias. + +### Patch Changes + +- 8a3a906: Deprecated `NavItemBlueprint`. Nav items are now automatically inferred from `PageBlueprint` extensions based on their `title` and `icon` params. +- b15a685: Deprecated `withApis`, use the `withApis` export from `@backstage/core-compat-api` instead. +- 0452d02: Add optional `description` field to plugin-level feature flags. +- 1bec049: Fixed inconsistent `JSX.Element` type reference in the `DialogApiDialog.update` method signature. +- 2c383b5: Deprecated `AnalyticsImplementationBlueprint` and `AnalyticsImplementationFactory` in favor of the exports from `@backstage/plugin-app-react`. +- dab6c46: Deprecated the `ExtensionFactoryMiddleware` type, which has been moved to `@backstage/frontend-app-api`. +- d0206c4: Removed the deprecated `defaultPath` migration helper from `PageBlueprint` params. +- edb872c: Renamed the `PageTab` type to `PageLayoutTab`. The old `PageTab` name is now a deprecated type alias. +- fe848e0: Changed `useApiHolder` to return an empty `ApiHolder` instead of throwing when used outside of an API context. + +## 0.14.2-next.0 + +### Patch Changes + +- 9c81af9: Made the `pluginId` property optional in the `FrontendFeature` type, allowing plugins published against older versions of the framework to be used without type errors. +- Updated dependencies + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## 0.14.0 + +### Minor Changes + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- bb9b471: Plugin IDs that do not match the standard format are deprecated (letters, digits, and dashes only, starting with a letter). Plugin IDs that do no match this format will be rejected in a future release. +- ef6916e: Added `SubPageBlueprint` for creating sub-page tabs, `PluginHeaderActionBlueprint` and `PluginHeaderActionsApi` for plugin-scoped header actions, and `PageLayout` as a swappable component. The `PageBlueprint` now supports sub-pages with tabbed navigation, page title, icon, and header actions. Plugins can now specify a `title` and `icon` in `createFrontendPlugin`. +- c38b74d: **BREAKING**: The following blueprints have been removed and are now only available from `@backstage/plugin-app-react`: + + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +- 10ebed4: **BREAKING**: Removed type support for multiple attachment points in the `ExtensionDefinitionAttachTo` type. Extensions can no longer specify an array of attachment points in the `attachTo` property. + + The runtime still supports multiple attachment points for backward compatibility with existing compiled code, but new code will receive type errors if attempting to use this pattern. + + Extensions that previously used multiple attachment points should migrate to using a Utility API pattern instead. See the [Sharing Extensions Across Multiple Locations](https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions) guide for the recommended approach. + +### Patch Changes + +- 7edb810: Added a new `internal` option to `createExtensionInput` that marks the input as only allowing attachments from the same plugin. +- 9554c36: **DEPRECATED**: Multiple attachment points for extensions have been deprecated. The functionality continues to work for backward compatibility, but will log a deprecation warning and be removed in a future release. + + Extensions using array attachment points should migrate to using Utility APIs instead. See the [Sharing Extensions Across Multiple Locations](https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions) guide for the recommended pattern. + +- 53b6549: Plugins in the new frontend system now have a `pluginId` field rather than `id` to better align with naming conventions used throughout the frontend and backend systems. The old field is still present but marked as deprecated. All internal code has been updated to prefer `pluginId` while maintaining backward compatibility by falling back to `id` when needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/version-bridge@1.0.12 + ## 0.14.0-next.2 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index aeffe8f3fc..568d542a38 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.14.0-next.2", + "version": "0.15.0-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/report-alpha.api.md b/packages/frontend-plugin-api/report-alpha.api.md index 77a377bb8e..76678107ef 100644 --- a/packages/frontend-plugin-api/report-alpha.api.md +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -11,8 +11,11 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ReactNode } from 'react'; -// @alpha +// @public export type PluginWrapperApi = { + getRootWrapper(): ComponentType<{ + children: ReactNode; + }>; getPluginWrapper(pluginId: string): | ComponentType<{ children: ReactNode; @@ -20,31 +23,19 @@ export type PluginWrapperApi = { | undefined; }; -// @alpha +// @public export const pluginWrapperApiRef: ApiRef; -// @alpha +// @public export const PluginWrapperBlueprint: ExtensionBlueprint<{ kind: 'plugin-wrapper'; - params: (params: { - loader: () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>; + params: (params: { + loader: () => Promise>; }) => ExtensionBlueprintParams<{ - loader: () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>; + loader: () => Promise; }>; output: ExtensionDataRef< - () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>, + () => Promise, 'core.plugin-wrapper.loader', {} >; @@ -53,16 +44,21 @@ export const PluginWrapperBlueprint: ExtensionBlueprint<{ configInput: {}; dataRefs: { wrapper: ConfigurableExtensionDataRef< - () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>, + () => Promise, 'core.plugin-wrapper.loader', {} >; }; }>; +// @public +export type PluginWrapperDefinition = { + useWrapperValue?: () => TValue; + component: ComponentType<{ + children: ReactNode; + value: TValue; + }>; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 43c48cdc13..d0fede34b6 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -13,10 +13,11 @@ import { ExpandRecursive } from '@backstage/types'; import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api'; +import { ExtensionInput as ExtensionInput_2 } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; -import { JSX as JSX_2 } from 'react/jsx-runtime'; -import { JSX as JSX_3 } from 'react'; +import { JSX as JSX_2 } from 'react'; +import { JSX as JSX_3 } from 'react/jsx-runtime'; import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; @@ -51,7 +52,7 @@ export const analyticsApiRef: ApiRef; export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; -}) => JSX_2.Element; +}) => JSX_3.Element; // @public export interface AnalyticsContextValue { @@ -80,7 +81,7 @@ export type AnalyticsImplementation = { captureEvent(event: AnalyticsEvent): void; }; -// @public +// @public @deprecated export const AnalyticsImplementationBlueprint: ExtensionBlueprint_2<{ kind: 'analytics'; params: ( @@ -103,7 +104,7 @@ export const AnalyticsImplementationBlueprint: ExtensionBlueprint_2<{ }; }>; -// @public (undocumented) +// @public @deprecated (undocumented) export type AnalyticsImplementationFactory< Deps extends { [name in string]: unknown; @@ -137,9 +138,6 @@ export type AnyApiFactory = ApiFactory< // @public export type AnyApiRef = ApiRef; -// @public @deprecated (undocumented) -export type AnyExtensionDataRef = ExtensionDataRef; - // @public export type AnyRouteRefParams = | { @@ -262,7 +260,7 @@ export const AppRootElementBlueprint: ExtensionBlueprint_2<{ params: { element: JSX.Element; }; - output: ExtensionDataRef_2; + output: ExtensionDataRef_2; inputs: {}; config: {}; configInput: {}; @@ -318,7 +316,7 @@ export const atlassianAuthApiRef: ApiRef< export type AuthProviderInfo = { id: string; title: string; - icon: IconComponent; + icon: IconComponent | IconElement; message?: string; }; @@ -388,8 +386,9 @@ export interface ConfigurableExtensionDataRef< // @public (undocumented) export const coreExtensionData: { title: ConfigurableExtensionDataRef_2; + icon: ConfigurableExtensionDataRef_2; reactElement: ConfigurableExtensionDataRef_2< - JSX_3.Element, + JSX_2.Element, 'core.reactElement', {} >; @@ -864,7 +863,7 @@ export interface DialogApiDialog { result(): Promise; update( elementOrComponent: - | React.JSX.Element + | JSX.Element | ((props: { dialog: DialogApiDialog }) => JSX.Element), ): void; } @@ -922,7 +921,7 @@ export interface Extension { // (undocumented) $$type: '@backstage/Extension'; // (undocumented) - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionAttachTo; // (undocumented) readonly configSchema?: PortableSchema; // (undocumented) @@ -932,18 +931,10 @@ export interface Extension { } // @public (undocumented) -export type ExtensionAttachTo = - | { - id: string; - input: string; - } - | Array<{ - id: string; - input: string; - }>; - -// @public @deprecated (undocumented) -export type ExtensionAttachToSpec = ExtensionAttachTo; +export type ExtensionAttachTo = { + id: string; + input: string; +}; // @public (undocumented) export interface ExtensionBlueprint< @@ -1101,7 +1092,7 @@ export type ExtensionBlueprintParams = { }; // @public (undocumented) -export function ExtensionBoundary(props: ExtensionBoundaryProps): JSX_2.Element; +export function ExtensionBoundary(props: ExtensionBoundaryProps): JSX_3.Element; // @public (undocumented) export namespace ExtensionBoundary { @@ -1165,12 +1156,6 @@ export type ExtensionDataRef< readonly config: TConfig; }; -// @public (undocumented) -export type ExtensionDataRefToValue = - TDataRef extends ExtensionDataRef - ? ExtensionDataValue - : never; - // @public (undocumented) export type ExtensionDataValue = { readonly $$type: '@backstage/ExtensionDataValue'; @@ -1224,7 +1209,7 @@ export type ExtensionDefinitionParameters = { params?: object | ExtensionBlueprintDefineParams; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type ExtensionFactoryMiddleware = ( originalFactory: (contextOverrides?: { config?: JsonObject; @@ -1288,6 +1273,7 @@ export type FeatureFlag = { // @public export type FeatureFlagConfig = { name: string; + description?: string; }; // @public @@ -1334,7 +1320,11 @@ export type FetchApi = { export const fetchApiRef: ApiRef; // @public (undocumented) -export type FrontendFeature = FrontendPlugin | FrontendModule; +export type FrontendFeature = + | (Omit & { + pluginId?: string; + }) + | FrontendModule; // @public (undocumented) export interface FrontendFeatureLoader { @@ -1367,12 +1357,14 @@ export interface FrontendPlugin< readonly $$type: '@backstage/FrontendPlugin'; // (undocumented) readonly externalRoutes: TExternalRoutes; + readonly icon?: IconElement; // @deprecated readonly id: string; info(): Promise; readonly pluginId: string; // (undocumented) readonly routes: TRoutes; + readonly title?: string; } // @public @@ -1420,15 +1412,19 @@ export const googleAuthApiRef: ApiRef< SessionApi >; -// @public +// @public @deprecated export type IconComponent = ComponentType<{ fontSize?: 'medium' | 'large' | 'small' | 'inherit'; }>; +// @public +export type IconElement = JSX_2.Element | null; + // @public export interface IconsApi { - // (undocumented) + // @deprecated (undocumented) getIcon(key: string): IconComponent | undefined; + icon(key: string): IconElement | undefined; // (undocumented) listIconKeys(): string[]; } @@ -1458,7 +1454,7 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public +// @public @deprecated export const NavItemBlueprint: ExtensionBlueprint_2<{ kind: 'nav-item'; params: { @@ -1699,7 +1695,9 @@ export interface OverridableFrontendPlugin< ): OverridableExtensionDefinition; // (undocumented) withOverrides(options: { - extensions: Array; + extensions?: Array; + title?: string; + icon?: IconElement; info?: FrontendPluginInfoOptions; }): OverridableFrontendPlugin; } @@ -1708,31 +1706,117 @@ export interface OverridableFrontendPlugin< export const PageBlueprint: ExtensionBlueprint_2<{ kind: 'page'; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; output: | ExtensionDataRef_2 - | ExtensionDataRef_2 | ExtensionDataRef_2< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef_2 + | ExtensionDataRef_2< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef_2< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput_2< + | ConfigurableExtensionDataRef_2 + | ConfigurableExtensionDataRef_2 + | ConfigurableExtensionDataRef_2< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef_2< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef_2< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; dataRefs: never; }>; +// @public +export const PageLayout: { + (props: PageLayoutProps): JSX.Element | null; + ref: SwappableComponentRef_2; +}; + +// @public +export interface PageLayoutProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + headerActions?: Array; + // (undocumented) + icon?: IconElement; + // (undocumented) + noHeader?: boolean; + // (undocumented) + tabs?: PageLayoutTab[]; + // (undocumented) + title?: string; +} + +// @public +export interface PageLayoutTab { + // (undocumented) + href: string; + // (undocumented) + icon?: IconElement; + // (undocumented) + id: string; + // (undocumented) + label: string; +} + +// @public @deprecated (undocumented) +export type PageTab = PageLayoutTab; + // @public export type PendingOAuthRequest = { provider: AuthProviderInfo; @@ -1740,6 +1824,29 @@ export type PendingOAuthRequest = { trigger(): Promise; }; +// @public +export const PluginHeaderActionBlueprint: ExtensionBlueprint_2<{ + kind: 'plugin-header-action'; + params: (params: { + loader: () => Promise; + }) => ExtensionBlueprintParams_2<{ + loader: () => Promise; + }>; + output: ExtensionDataRef_2; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: never; +}>; + +// @public +export type PluginHeaderActionsApi = { + getPluginHeaderActions(pluginId: string): Array; +}; + +// @public +export const pluginHeaderActionsApiRef: ApiRef_2; + // @public (undocumented) export interface PluginOptions< TId extends string, @@ -1757,14 +1864,65 @@ export interface PluginOptions< externalRoutes?: TExternalRoutes; // (undocumented) featureFlags?: FeatureFlagConfig[]; + icon?: IconElement; // (undocumented) info?: FrontendPluginInfoOptions; // (undocumented) pluginId: TId; // (undocumented) routes?: TRoutes; + title?: string; } +// @public +export type PluginWrapperApi = { + getRootWrapper(): ComponentType<{ + children: ReactNode; + }>; + getPluginWrapper(pluginId: string): + | ComponentType<{ + children: ReactNode; + }> + | undefined; +}; + +// @public +export const pluginWrapperApiRef: ApiRef_2; + +// @public +export const PluginWrapperBlueprint: ExtensionBlueprint_2<{ + kind: 'plugin-wrapper'; + params: (params: { + loader: () => Promise>; + }) => ExtensionBlueprintParams_2<{ + loader: () => Promise; + }>; + output: ExtensionDataRef_2< + () => Promise, + 'core.plugin-wrapper.loader', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + wrapper: ConfigurableExtensionDataRef_2< + () => Promise, + 'core.plugin-wrapper.loader', + {} + >; + }; +}>; + +// @public +export type PluginWrapperDefinition = { + useWrapperValue?: () => TValue; + component: ComponentType<{ + children: ReactNode; + value: TValue; + }>; +}; + // @public (undocumented) export type PortableSchema = { parse: (input: TInput) => TOutput; @@ -1792,14 +1950,6 @@ export const Progress: { // @public (undocumented) export type ProgressProps = {}; -// @public -export type ResolvedExtensionInput = - TExtensionInput['extensionData'] extends Array - ? { - node: AppNode; - } & ExtensionDataContainer - : never; - // @public export type ResolvedExtensionInputs< TInputs extends { @@ -1815,9 +1965,7 @@ export type ResolvedExtensionInputs< // @public export type RouteFunc = ( - ...[params]: TParams extends undefined - ? readonly [] - : readonly [params: TParams] + ...input: TParams extends undefined ? readonly [] : readonly [params: TParams] ) => string; // @public @@ -1898,6 +2046,46 @@ export type StorageValueSnapshot = value: TValue; }; +// @public +export const SubPageBlueprint: ExtensionBlueprint_2<{ + kind: 'sub-page'; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef; + }; + output: + | ExtensionDataRef_2 + | ExtensionDataRef_2< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef_2 + | ExtensionDataRef_2 + | ExtensionDataRef_2< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + dataRefs: never; +}>; + // @public export interface SubRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1995,7 +2183,7 @@ export type TranslationFunction< ? { ( key: TKey, - ...[args]: TranslationFunctionOptions< + ...input: TranslationFunctionOptions< NestedMessageKeys, PluralKeys, IMessages, @@ -2004,13 +2192,13 @@ export type TranslationFunction< ): IMessages[TKey]; ( key: TKey, - ...[args]: TranslationFunctionOptions< + ...input: TranslationFunctionOptions< NestedMessageKeys, PluralKeys, IMessages, - string | JSX_3.Element + string | JSX_2.Element > - ): JSX_3.Element; + ): JSX_2.Element; } : never; @@ -2180,13 +2368,13 @@ export const vmwareCloudAuthApiRef: ApiRef< SessionApi >; -// @public +// @public @deprecated export function withApis( apis: TypesToApiRefs, ): ( WrappedComponent: ComponentType, ) => { - (props: PropsWithChildren>): JSX_2.Element; + (props: PropsWithChildren>): JSX_3.Element; displayName: string; }; ``` diff --git a/packages/frontend-plugin-api/src/alpha.ts b/packages/frontend-plugin-api/src/alpha.ts index 2251bfafb7..dfd7f1e2bf 100644 --- a/packages/frontend-plugin-api/src/alpha.ts +++ b/packages/frontend-plugin-api/src/alpha.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -export { PluginWrapperBlueprint } from './blueprints/PluginWrapperBlueprint'; +// These exports are now available from the main entry point and are +// re-exported here only for backwards compatibility. +export { + PluginWrapperBlueprint, + type PluginWrapperDefinition, +} from './blueprints/PluginWrapperBlueprint'; export { type PluginWrapperApi, pluginWrapperApiRef, diff --git a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts index 85151b05f6..d7ab2e5d75 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts @@ -42,7 +42,7 @@ export interface DialogApiDialog { */ update( elementOrComponent: - | React.JSX.Element + | JSX.Element | ((props: { dialog: DialogApiDialog }) => JSX.Element), ): void; diff --git a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts index fbc9928dc1..d22ebcce4a 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts @@ -15,7 +15,7 @@ */ import { createApiRef } from '../system'; -import { IconComponent } from '../../icons'; +import { IconComponent, IconElement } from '../../icons'; /** * API for accessing app icons. @@ -23,6 +23,14 @@ import { IconComponent } from '../../icons'; * @public */ export interface IconsApi { + /** + * Look up an icon element by key. + */ + icon(key: string): IconElement | undefined; + + /** + * @deprecated Use {@link IconsApi.icon} instead. + */ getIcon(key: string): IconComponent | undefined; listIconKeys(): string[]; diff --git a/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts new file mode 100644 index 0000000000..78d0e2623f --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2026 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 { JSX } from 'react'; +import { createApiRef } from '../system'; + +/** + * API for retrieving plugin-scoped header actions. + * + * @remarks + * + * Header actions are provided via + * {@link @backstage/frontend-plugin-api#PluginHeaderActionBlueprint} + * and automatically scoped to the providing plugin. + * + * @public + */ +export type PluginHeaderActionsApi = { + /** + * Returns the header actions for a given plugin. + */ + getPluginHeaderActions(pluginId: string): Array; +}; + +/** + * The `ApiRef` of {@link PluginHeaderActionsApi}. + * + * @public + */ +export const pluginHeaderActionsApiRef = createApiRef({ + id: 'core.plugin-header-actions', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts index a4b66d6128..8d9b224b1f 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts @@ -15,21 +15,23 @@ */ import { ComponentType, ReactNode } from 'react'; -import { createApiRef } from '@backstage/frontend-plugin-api'; +import { createApiRef } from '../system'; /** - * The Plugin Wrapper API is used to wrap plugin extensions with providers, - * plugins should generally use `ExtensionBoundary` instead. + * The Plugin Wrapper API allows plugins to wrap their extensions with + * providers. This API is only intended for internal use by the Backstage + * frontend system. To provide contexts to plugin components, use + * `ExtensionBoundary` instead. * - * @remarks - * - * This API is primarily intended for internal use by the Backstage frontend - * system, but can be used for advanced use-cases. If you do override it, be - * sure to include the default implementation as well. - * - * @alpha + * @public */ export type PluginWrapperApi = { + /** + * Returns the root wrapper that manages the global plugin state across + * plugin wrapper instances. + */ + getRootWrapper(): ComponentType<{ children: ReactNode }>; + /** * Returns a wrapper component for a specific plugin, or undefined if no * wrappers exist. Do not use this API directly, instead use @@ -43,8 +45,8 @@ export type PluginWrapperApi = { /** * The API reference of {@link PluginWrapperApi}. * - * @alpha + * @public */ export const pluginWrapperApiRef = createApiRef({ - id: 'core.plugin-wrapper.alpha', + id: 'core.plugin-wrapper', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts index 2578eaf7c4..b569800e3e 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts @@ -22,7 +22,7 @@ import { JSX } from 'react'; /** * Base translation options. * - * @alpha + * @ignore */ interface BaseOptions { interpolation?: { diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 9a0ddcd958..76aa24e8f8 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -17,7 +17,7 @@ /* eslint-disable @typescript-eslint/no-redeclare */ import { ApiRef, createApiRef } from '../system'; -import { IconComponent } from '../../icons/types'; +import { IconComponent, IconElement } from '../../icons/types'; import { Observable } from '@backstage/types'; /** @@ -54,8 +54,13 @@ export type AuthProviderInfo = { /** * Icon for the auth provider. + * + * @remarks + * + * Accepts either an `IconElement` (e.g. ``) or an `IconComponent` + * (e.g. `MyIcon`). Prefer passing `IconElement`. */ - icon: IconComponent; + icon: IconComponent | IconElement; /** * Optional user friendly messaage to display for the auth provider. diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index 41dce8f915..e33834ff5e 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -50,3 +50,5 @@ export * from './StorageApi'; export * from './AnalyticsApi'; export * from './ToastApi'; export * from './TranslationApi'; +export * from './PluginHeaderActionsApi'; +export * from './PluginWrapperApi'; diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx index 4b9d80fb62..7596105810 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx @@ -17,7 +17,30 @@ import { renderHook } from '@testing-library/react'; import { createVersionedContextForTesting } from '@backstage/version-bridge'; import { createApiRef } from './ApiRef'; -import { useApi } from './useApi'; +import { useApi, useApiHolder } from './useApi'; + +describe('useApiHolder', () => { + const context = createVersionedContextForTesting('api-context'); + + afterEach(() => { + context.reset(); + }); + + it('should return the API holder from context', () => { + const holder = { get: jest.fn() }; + context.set({ 1: holder }); + + const renderedHook = renderHook(() => useApiHolder()); + expect(renderedHook.result.current).toBe(holder); + }); + + it('should return an empty API holder when there is no context', () => { + const renderedHook = renderHook(() => useApiHolder()); + + const holder = renderedHook.result.current; + expect(holder.get(createApiRef({ id: 'x' }))).toBeUndefined(); + }); +}); describe('useApi', () => { const context = createVersionedContextForTesting('api-context'); diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx index 30e39033f7..2808f13f1f 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -19,6 +19,8 @@ import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { useVersionedContext } from '@backstage/version-bridge'; import { NotImplementedError } from '@backstage/errors'; +const emptyApiHolder: ApiHolder = Object.freeze({ get: () => undefined }); + /** * React hook for retrieving {@link ApiHolder}, an API catalog. * @@ -27,7 +29,7 @@ import { NotImplementedError } from '@backstage/errors'; export function useApiHolder(): ApiHolder { const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); if (!versionedHolder) { - throw new NotImplementedError('API context is not available'); + return emptyApiHolder; } const apiHolder = versionedHolder.atVersion(1); @@ -57,6 +59,7 @@ export function useApi(apiRef: ApiRef): T { * Wrapper for giving component an API context. * * @param apis - APIs for the context. + * @deprecated Use `withApis` from `@backstage/core-compat-api` instead. * @public */ export function withApis(apis: TypesToApiRefs) { diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts index 23b5982738..e9961c3d48 100644 --- a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts @@ -21,7 +21,10 @@ import { createExtensionDataRef, } from '../wiring'; -/** @public */ +/** + * @public + * @deprecated Use `AnalyticsImplementationFactory` from `@backstage/plugin-app-react` instead. + */ export type AnalyticsImplementationFactory< Deps extends { [name in string]: unknown } = {}, > = { @@ -38,6 +41,7 @@ const factoryDataRef = * Creates analytics implementations. * * @public + * @deprecated Use `AnalyticsImplementationBlueprint` from `@backstage/plugin-app-react` instead. */ export const AnalyticsImplementationBlueprint = createExtensionBlueprint({ kind: 'analytics', diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index ce59a9439b..057bb7ca54 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -29,6 +29,10 @@ const targetDataRef = createExtensionDataRef<{ * Creates extensions that make up the items of the nav bar. * * @public + * @deprecated Nav items are now automatically inferred from `PageBlueprint` + * extensions based on their `title` and `icon` params. You can remove your + * `NavItemBlueprint` usage and instead pass `title` and `icon` directly to + * the `PageBlueprint`. */ export const NavItemBlueprint = createExtensionBlueprint({ kind: 'nav-item', diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index c5c34fc02f..8fc9c56bfe 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -56,13 +56,63 @@ describe('PageBlueprint', () => { "path": { "type": "string", }, + "title": { + "type": "string", + }, }, "type": "object", }, }, "disabled": false, "factory": [Function], - "inputs": {}, + "inputs": { + "pages": { + "$$type": "@backstage/ExtensionInput", + "config": { + "internal": false, + "optional": false, + "singleton": false, + }, + "context": { + "input": "pages", + "kind": "page", + "name": "test-page", + }, + "extensionData": [ + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.title", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.icon", + "optional": [Function], + "toString": [Function], + }, + ], + "replaces": undefined, + "withContext": [Function], + }, + }, "kind": "page", "name": "test-page", "output": [ @@ -77,6 +127,24 @@ describe('PageBlueprint', () => { "optional": [Function], "toString": [Function], }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.title", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.icon", + "optional": [Function], + "toString": [Function], + }, ], "override": [Function], "toString": [Function], diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 92e2ab6172..513da27faa 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -14,46 +14,152 @@ * limitations under the License. */ +import { JSX } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { IconElement } from '../icons/types'; import { RouteRef } from '../routing'; -import { coreExtensionData, createExtensionBlueprint } from '../wiring'; -import { ExtensionBoundary } from '../components'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, +} from '../wiring'; +import { ExtensionBoundary, PageLayout, PageLayoutTab } from '../components'; +import { useApi } from '../apis/system'; +import { pluginHeaderActionsApiRef } from '../apis/definitions/PluginHeaderActionsApi'; /** - * Createx extensions that are routable React page components. + * Creates extensions that are routable React page components. * * @public */ export const PageBlueprint = createExtensionBlueprint({ kind: 'page', attachTo: { id: 'app/routes', input: 'routes' }, + inputs: { + pages: createExtensionInput([ + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + coreExtensionData.reactElement, + coreExtensionData.title.optional(), + coreExtensionData.icon.optional(), + ]), + }, output: [ coreExtensionData.routePath, coreExtensionData.reactElement, coreExtensionData.routeRef.optional(), + coreExtensionData.title.optional(), + coreExtensionData.icon.optional(), ], config: { schema: { path: z => z.string().optional(), + title: z => z.string().optional(), }, }, *factory( params: { - /** - * @deprecated Use the `path` param instead. - */ - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + /** + * Hide the default plugin page header, making the page fill up all available space. + */ + noHeader?: boolean; }, - { config, node }, + { config, node, inputs }, ) { + const title = config.title ?? params.title; + const icon = params.icon; + const pluginId = node.spec.plugin.pluginId; + const noHeader = params.noHeader ?? false; + yield coreExtensionData.routePath(config.path ?? params.path); - yield coreExtensionData.reactElement( - ExtensionBoundary.lazy(node, params.loader), - ); + if (params.loader) { + const loader = params.loader; + const PageContent = () => { + const headerActionsApi = useApi(pluginHeaderActionsApiRef); + const headerActions = headerActionsApi.getPluginHeaderActions(pluginId); + + return ( + + {ExtensionBoundary.lazy(node, loader)} + + ); + }; + yield coreExtensionData.reactElement(); + } else if (inputs.pages.length > 0) { + // Parent page with sub-pages - render header with tabs + const tabs: PageLayoutTab[] = inputs.pages.map(page => { + const path = page.get(coreExtensionData.routePath); + const tabTitle = page.get(coreExtensionData.title); + const tabIcon = page.get(coreExtensionData.icon); + return { + id: path, + label: tabTitle || path, + icon: tabIcon, + href: path, + }; + }); + + const PageContent = () => { + const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath); + + const headerActionsApi = useApi(pluginHeaderActionsApiRef); + const headerActions = headerActionsApi.getPluginHeaderActions(pluginId); + + return ( + + + {firstPagePath && ( + } + /> + )} + {inputs.pages.map((page, index) => { + const path = page.get(coreExtensionData.routePath); + const element = page.get(coreExtensionData.reactElement); + return ( + + ); + })} + + + ); + }; + + yield coreExtensionData.reactElement(); + } else { + const PageContent = () => { + const headerActionsApi = useApi(pluginHeaderActionsApiRef); + const headerActions = headerActionsApi.getPluginHeaderActions(pluginId); + return ( + + ); + }; + yield coreExtensionData.reactElement(); + } if (params.routeRef) { yield coreExtensionData.routeRef(params.routeRef); } + if (title) { + yield coreExtensionData.title(title); + } + if (icon) { + yield coreExtensionData.icon(icon); + } }, }); diff --git a/packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx new file mode 100644 index 0000000000..84ed9da7ff --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PluginHeaderActionBlueprint.tsx @@ -0,0 +1,52 @@ +/* + * 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 { lazy as reactLazy } from 'react'; +import { ExtensionBoundary } from '../components'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionBlueprintParams, +} from '../wiring'; + +/** + * Creates extensions that provide plugin-scoped header actions. + * + * @remarks + * + * These actions are automatically scoped to the plugin that provides them + * and will appear in the header of all pages belonging to that plugin. + * + * @public + */ +export const PluginHeaderActionBlueprint = createExtensionBlueprint({ + kind: 'plugin-header-action', + attachTo: { id: 'api:app/plugin-header-actions', input: 'actions' }, + output: [coreExtensionData.reactElement], + defineParams(params: { loader: () => Promise }) { + return createExtensionBlueprintParams(params); + }, + *factory(params, { node }) { + const LazyAction = reactLazy(() => + params.loader().then(element => ({ default: () => element })), + ); + yield coreExtensionData.reactElement( + + + , + ); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx index f6f673bd93..5119ae1617 100644 --- a/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PluginWrapperBlueprint.tsx @@ -21,14 +21,42 @@ import { createExtensionDataRef, } from '../wiring'; +/** + * Defines the structure of a plugin wrapper, optionally including a shared + * hook value. + * + * @remarks + * + * When `useWrapperValue` is provided, the hook is called in a single location + * in the app and the resulting value is forwarded as the `value` prop to the + * component. The hook obeys the rules of React hooks and is not called until a + * component from the plugin is rendered. + * + * @public + */ +export type PluginWrapperDefinition = { + /** + * Creates a shared value that is forwarded as the `value` prop to the + * component. + * + * @remarks + * + * This function obeys the rules of React hooks and is only invoked in a + * single location in the app. Note that the hook will not be called until a + * component from the plugin is rendered. + */ + useWrapperValue?: () => TValue; + component: ComponentType<{ children: ReactNode; value: TValue }>; +}; + const wrapperDataRef = createExtensionDataRef< - () => Promise<{ component: ComponentType<{ children: ReactNode }> }> + () => Promise >().with({ id: 'core.plugin-wrapper.loader' }); /** * Creates extensions that wrap plugin extensions with providers. * - * @alpha + * @public */ export const PluginWrapperBlueprint = createExtensionBlueprint({ kind: 'plugin-wrapper', @@ -37,12 +65,12 @@ export const PluginWrapperBlueprint = createExtensionBlueprint({ dataRefs: { wrapper: wrapperDataRef, }, - defineParams(params: { - loader: () => Promise<{ - component: ComponentType<{ children: ReactNode }>; - }>; + defineParams(params: { + loader: () => Promise>; }) { - return createExtensionBlueprintParams(params); + return createExtensionBlueprintParams( + params as { loader: () => Promise }, + ); }, *factory(params) { yield wrapperDataRef(params.loader); diff --git a/packages/frontend-plugin-api/src/blueprints/SubPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SubPageBlueprint.tsx new file mode 100644 index 0000000000..1d2cd47984 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SubPageBlueprint.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2026 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 { IconElement } from '../icons/types'; +import { RouteRef } from '../routing'; +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +import { ExtensionBoundary } from '../components'; + +/** + * Creates extensions that are sub-page React components attached to a parent page. + * Sub-pages are rendered as tabs within the parent page's header. + * + * @public + * @example + * ```tsx + * const overviewRouteRef = createRouteRef(); + * + * const mySubPage = SubPageBlueprint.make({ + * attachTo: { id: 'page:my-plugin', input: 'pages' }, + * name: 'overview', + * params: { + * path: 'overview', + * title: 'Overview', + * routeRef: overviewRouteRef, + * loader: () => import('./components/Overview').then(m => ), + * }, + * }); + * ``` + */ +export const SubPageBlueprint = createExtensionBlueprint({ + kind: 'sub-page', + attachTo: { relative: { kind: 'page' }, input: 'pages' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.title, + coreExtensionData.routeRef.optional(), + coreExtensionData.icon.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + title: z => z.string().optional(), + }, + }, + *factory( + params: { + /** + * The path for this sub-page, relative to the parent page. Must **not** start with '/'. + * + * @example 'overview', 'settings', 'details' + */ + path: string; + /** + * The title displayed in the tab for this sub-page. + */ + title: string; + /** + * Optional icon for this sub-page, displayed in the tab. + */ + icon?: IconElement; + /** + * A function that returns a promise resolving to the React element to render. + * This enables lazy loading of the sub-page content. + */ + loader: () => Promise; + /** + * Optional route reference for this sub-page. + */ + routeRef?: RouteRef; + }, + { config, node }, + ) { + yield coreExtensionData.routePath(config.path ?? params.path); + yield coreExtensionData.title(config.title ?? params.title); + yield coreExtensionData.reactElement( + ExtensionBoundary.lazy(node, params.loader), + ); + if (params.routeRef) { + yield coreExtensionData.routeRef(params.routeRef); + } + if (params.icon) { + yield coreExtensionData.icon(params.icon); + } + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index 344b44dbff..a571f6ef73 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -22,3 +22,9 @@ export { ApiBlueprint } from './ApiBlueprint'; export { AppRootElementBlueprint } from './AppRootElementBlueprint'; export { NavItemBlueprint } from './NavItemBlueprint'; export { PageBlueprint } from './PageBlueprint'; +export { SubPageBlueprint } from './SubPageBlueprint'; +export { PluginHeaderActionBlueprint } from './PluginHeaderActionBlueprint'; +export { + PluginWrapperBlueprint, + type PluginWrapperDefinition, +} from './PluginWrapperBlueprint'; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 1215c7576f..d05e7c366f 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -144,6 +144,10 @@ describe('ExtensionBoundary', () => { }; const pluginWrapperApi: PluginWrapperApi = { + getRootWrapper: + () => + ({ children }: { children: ReactNode }) => + <>{children}, getPluginWrapper: jest.fn((pluginId: string) => { if (pluginId === 'app') { return WrapperComponent; @@ -180,6 +184,10 @@ describe('ExtensionBoundary', () => { }; const pluginWrapperApi: PluginWrapperApi = { + getRootWrapper: + () => + ({ children }: { children: ReactNode }) => + <>{children}, getPluginWrapper: jest.fn((pluginId: string) => { if (pluginId === 'app') { return ThrowingWrapper; diff --git a/packages/frontend-plugin-api/src/components/PageLayout.tsx b/packages/frontend-plugin-api/src/components/PageLayout.tsx new file mode 100644 index 0000000000..d97d746665 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/PageLayout.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2024 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 { ReactNode } from 'react'; +import { IconElement } from '../icons/types'; +import { createSwappableComponent } from './createSwappableComponent'; + +/** + * Tab configuration for page navigation + * @public + */ +export interface PageLayoutTab { + id: string; + label: string; + icon?: IconElement; + href: string; +} + +/** + * @deprecated Use {@link PageLayoutTab} instead + * @public + */ +export type PageTab = PageLayoutTab; + +/** + * Props for the PageLayout component + * @public + */ +export interface PageLayoutProps { + title?: string; + icon?: IconElement; + noHeader?: boolean; + headerActions?: Array; + tabs?: PageLayoutTab[]; + children?: ReactNode; +} + +/** + * Default implementation of PageLayout using plain HTML elements + */ +function DefaultPageLayout(props: PageLayoutProps): JSX.Element { + const { title, icon, headerActions, tabs, children } = props; + + return ( +
    + {(title || tabs) && ( +
    + {title && ( +
    + {icon} + {title} + {headerActions && ( +
    {headerActions}
    + )} +
    + )} + {tabs && tabs.length > 0 && ( + + )} +
    + )} +
    + {children} +
    +
    + ); +} + +/** + * Swappable component for laying out page content with header and navigation. + * The default implementation uses plain HTML elements. + * Apps can override this with a custom implementation (e.g., using \@backstage/ui). + * + * @public + */ +export const PageLayout = createSwappableComponent({ + id: 'core.page-layout', + loader: () => DefaultPageLayout, +}); diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 450224bdc4..7cb109932a 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -25,3 +25,9 @@ export { } from './createSwappableComponent'; export { useAppNode } from './AppNodeProvider'; export * from './DefaultSwappableComponents'; +export { + PageLayout, + type PageLayoutProps, + type PageLayoutTab, + type PageTab, +} from './PageLayout'; diff --git a/packages/frontend-plugin-api/src/icons/index.ts b/packages/frontend-plugin-api/src/icons/index.ts index 9c9e45e54c..913f38d5bf 100644 --- a/packages/frontend-plugin-api/src/icons/index.ts +++ b/packages/frontend-plugin-api/src/icons/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { IconComponent } from './types'; +export type { IconComponent, IconElement } from './types'; diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts index 2b00e1456f..180763ad09 100644 --- a/packages/frontend-plugin-api/src/icons/types.ts +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ComponentType } from 'react'; +import { ComponentType, JSX } from 'react'; /** * IconComponent is the common icon type used throughout Backstage when @@ -31,7 +31,26 @@ import { ComponentType } from 'react'; * also describe your use-case and reasoning of the addition. * * @public + * @deprecated Use {@link IconElement} instead, passing `` rather than `MyIcon`. */ export type IconComponent = ComponentType<{ fontSize?: 'medium' | 'large' | 'small' | 'inherit'; }>; + +/** + * The type used for icon elements throughout Backstage. + * + * @remarks + * + * Icon elements should behave like rendering a plain icon directly, for example + * from `@remixicon/react`, and are expected to be sized by the surrounding UI. + * Icons should be exactly 24x24 pixels in size by default. + * + * Using icons from `@remixicon/react` is preferred. Using icons from + * `@material-ui/icons` or `AppIcon` and its variants from + * `@backstage/core-components` is supported while migrating, but deprecated. + * When using those icons, you must set `fontSize="inherit"` on the element. + * + * @public + */ +export type IconElement = JSX.Element | null; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index b778539101..9b89fe03cb 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -15,12 +15,15 @@ */ import { JSX } from 'react'; +import { IconElement } from '../icons/types'; import { RouteRef } from '../routing/RouteRef'; import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { title: createExtensionDataRef().with({ id: 'core.title' }), + /** An icon element for the extension. Should be exactly 24x24 pixels. */ + icon: createExtensionDataRef().with({ id: 'core.icon' }), reactElement: createExtensionDataRef().with({ id: 'core.reactElement', }), diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 4d3eeece2c..329205ae76 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -43,11 +43,8 @@ import { FrontendModule } from './createFrontendModule'; */ export const ctxParamsSymbol = Symbol('params'); -/** - * Convert a single extension input into a matching resolved input. - * @public - */ -export type ResolvedExtensionInput = +/** @ignore */ +type ResolvedExtensionInput = TExtensionInput['extensionData'] extends Array ? { node: AppNode; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index 1bfbb01740..8ae176d249 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -33,18 +33,12 @@ export type ExtensionDataRef< readonly config: TConfig; }; -/** @public */ -export type ExtensionDataRefToValue = +/** @ignore */ +export type ExtensionDataRefToValue = TDataRef extends ExtensionDataRef ? ExtensionDataValue : never; -/** - * @deprecated Use `ExtensionDataRef` without type parameters instead. - * @public - */ -export type AnyExtensionDataRef = ExtensionDataRef; - /** @public */ export interface ConfigurableExtensionDataRef< TData, diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts index 62fd1e88ff..08329b6fd4 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts @@ -29,6 +29,7 @@ import { import { FeatureFlagConfig } from './types'; import { MakeSortedExtensionsMap } from './MakeSortedExtensionsMap'; import { JsonObject } from '@backstage/types'; +import { IconElement } from '../icons/types'; import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing'; import { ID_PATTERN } from './constants'; @@ -112,7 +113,17 @@ export interface OverridableFrontendPlugin< id: TId, ): OverridableExtensionDefinition; withOverrides(options: { - extensions: Array; + extensions?: Array; + + /** + * Overrides the display title of the plugin. + */ + title?: string; + + /** + * Overrides the display icon of the plugin. + */ + icon?: IconElement; /** * Overrides the original info loaders of the plugin one by one. @@ -141,6 +152,15 @@ export interface FrontendPlugin< * @deprecated Use `pluginId` instead. */ readonly id: string; + /** + * The display title of the plugin, used in page headers and navigation. + * Falls back to the plugin ID if not provided. + */ + readonly title?: string; + /** + * The display icon of the plugin, used in page headers and navigation. + */ + readonly icon?: IconElement; readonly routes: TRoutes; readonly externalRoutes: TExternalRoutes; @@ -158,6 +178,15 @@ export interface PluginOptions< TExtensions extends readonly ExtensionDefinition[], > { pluginId: TId; + /** + * The display title of the plugin, used in page headers and navigation. + * Falls back to the plugin ID if not provided. + */ + title?: string; + /** + * The display icon of the plugin, used in page headers and navigation. + */ + icon?: IconElement; routes?: TRoutes; externalRoutes?: TExternalRoutes; extensions?: TExtensions; @@ -250,6 +279,8 @@ export function createFrontendPlugin< return OpaqueFrontendPlugin.createInstance('v1', { pluginId, id: pluginId, + title: options.title, + icon: options.icon, routes: options.routes ?? ({} as TRoutes), externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes), featureFlags: options.featureFlags ?? [], @@ -275,8 +306,9 @@ export function createFrontendPlugin< return `Plugin{id=${pluginId}}`; }, withOverrides(overrides) { + const overrideExtensions = overrides.extensions ?? []; const overriddenExtensionIds = new Set( - overrides.extensions.map( + overrideExtensions.map( e => resolveExtensionDefinition(e, { namespace: pluginId }).id, ), ); @@ -289,7 +321,9 @@ export function createFrontendPlugin< return createFrontendPlugin({ ...options, pluginId, - extensions: [...nonOverriddenExtensions, ...overrides.extensions], + title: overrides.title ?? options.title, + icon: overrides.icon ?? options.icon, + extensions: [...nonOverriddenExtensions, ...overrideExtensions], info: { ...options.info, ...overrides.info, diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 6ce1bb4998..d3acef46b6 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -22,7 +22,6 @@ export { type ExtensionDefinitionParameters, type CreateExtensionOptions, type OverridableExtensionDefinition, - type ResolvedExtensionInput, type ResolvedExtensionInputs, } from './createExtension'; export { @@ -31,9 +30,7 @@ export { } from './createExtensionInput'; export { createExtensionDataRef, - type AnyExtensionDataRef, type ExtensionDataRef, - type ExtensionDataRefToValue, type ExtensionDataValue, type ConfigurableExtensionDataRef, } from './createExtensionDataRef'; @@ -58,7 +55,6 @@ export { export { type Extension, type ExtensionAttachTo, - type ExtensionAttachToSpec, } from './resolveExtensionDefinition'; export { type ExtensionDataContainer, diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 605e75bfe8..70a0bd7837 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -30,21 +30,13 @@ import { } from '@internal/frontend'; /** @public */ -export type ExtensionAttachTo = - | { id: string; input: string } - | Array<{ id: string; input: string }>; - -/** - * @deprecated Use {@link ExtensionAttachTo} instead. - * @public - */ -export type ExtensionAttachToSpec = ExtensionAttachTo; +export type ExtensionAttachTo = { id: string; input: string }; /** @public */ export interface Extension { $$type: '@backstage/Extension'; readonly id: string; - readonly attachTo: ExtensionAttachToSpec; + readonly attachTo: ExtensionAttachTo; readonly disabled: boolean; readonly configSchema?: PortableSchema; } @@ -151,7 +143,7 @@ function resolveExtensionId( function resolveAttachTo( attachTo: ExtensionDefinitionAttachTo | ExtensionDefinitionAttachTo[], namespace?: string, -): ExtensionAttachToSpec { +): ExtensionAttachTo | ExtensionAttachTo[] { const resolveSpec = ( spec: ExtensionDefinitionAttachTo, ): { id: string; input: string } => { @@ -213,7 +205,7 @@ export function resolveExtensionDefinition< return { ...rest, - attachTo: resolveAttachTo(attachTo, namespace), + attachTo: resolveAttachTo(attachTo, namespace) as ExtensionAttachTo, $$type: '@backstage/Extension', version: internalDefinition.version, id, diff --git a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts index b9f239842d..9a122c8288 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts @@ -16,7 +16,6 @@ import { AppNode } from '../apis'; import { Expand } from '@backstage/types'; -import { ResolvedExtensionInput } from './createExtension'; import { createExtensionDataContainer } from '@internal/frontend'; import { ExtensionDataRefToValue, @@ -124,7 +123,7 @@ export function resolveInputOverrides( ); } newInputs[name] = Object.assign(providedContainer, { - node: (originalInput as ResolvedExtensionInput).node, + node: (originalInput as { node: AppNode }).node, }) as any; } } else { @@ -158,7 +157,7 @@ export function resolveInputOverrides( declaredInput.extensionData, ); return Object.assign(providedContainer, { - node: (originalInput[i] as ResolvedExtensionInput).node, + node: (originalInput[i] as { node: AppNode }).node, }) as any; }); } else if (withNodesCount === providedData.length) { diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 460a399871..39f0c5b03c 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -29,6 +29,8 @@ import { FrontendPlugin } from './createFrontendPlugin'; export type FeatureFlagConfig = { /** Feature flag name */ name: string; + /** Feature flag description */ + description?: string; }; /** @public */ @@ -60,7 +62,10 @@ export type ExtensionDataContainer = : never; }; -/** @public */ +/** + * @public + * @deprecated Moved to {@link @backstage/frontend-app-api#ExtensionFactoryMiddleware} + */ export type ExtensionFactoryMiddleware = ( originalFactory: (contextOverrides?: { config?: JsonObject; @@ -73,4 +78,6 @@ export type ExtensionFactoryMiddleware = ( ) => Iterable>; /** @public */ -export type FrontendFeature = FrontendPlugin | FrontendModule; +export type FrontendFeature = + | (Omit & { pluginId?: string }) + | FrontendModule; diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 13f426ef46..07029c210c 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,147 @@ # @backstage/frontend-test-utils +## 0.5.1-next.2 + +### Patch Changes + +- b56f573: Deprecated standalone mock API exports in favor of the `mockApis` namespace. This includes the mock classes (`MockAlertApi`, `MockAnalyticsApi`, `MockConfigApi`, `MockErrorApi`, `MockFetchApi`, `MockFeatureFlagsApi`, `MockPermissionApi`, `MockStorageApi`, `MockTranslationApi`), their option types (`MockErrorApiOptions`, `MockFeatureFlagsApiOptions`), and the `ErrorWithContext` type. `MockFetchApiOptions` is kept as a non-deprecated export. Use the `mockApis` namespace instead, for example `mockApis.alert()` or `mockApis.alert.mock()`. +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/frontend-app-api@0.16.0-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/plugin-app@0.4.1-next.2 + +## 0.5.1-next.1 + +### Patch Changes + +- 479282f: Fixed type inference of `TestApiPair` when using tuple syntax by wrapping `MockWithApiFactory` in `NoInfer`. +- Updated dependencies + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.5.1-next.0 + +### Patch Changes + +- 909c742: Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.5.0 + +### Minor Changes + +- 09a6aad: **BREAKING**: Removed the `TestApiRegistry` class, use `TestApiProvider` directly instead, storing reused APIs in a variable, e.g. `const apis = [...] as const`. +- d2ac2ec: Added `MockAlertApi` and `MockFeatureFlagsApi` implementations to the `mockApis` namespace. The mock implementations include useful testing methods like `clearAlerts()`, `waitForAlert()`, `getState()`, `setState()`, and `clearState()` for better test ergonomics. +- 09a6aad: **BREAKING**: The `mockApis` namespace is no longer a re-export from `@backstage/test-utils`. It's now a standalone namespace with mock implementations of most core APIs. Mock API instances can be passed directly to `TestApiProvider`, `renderInTestApp`, and `renderTestApp` without needing `[apiRef, impl]` tuples. As part of this change, the `.factory()` method on some mocks has been removed, since it's now redundant. + + ```tsx + // Before + import { mockApis } from '@backstage/frontend-test-utils'; + + renderInTestApp(, { + apis: [[identityApiRef, mockApis.identity()]], + }); + + // After - mock APIs can be passed directly + renderInTestApp(, { + apis: [mockApis.identity()], + }); + ``` + + This change also adds `createApiMock`, a public utility for creating mock API factories, intended for plugin authors to create their own `.mock()` variants. + +### Patch Changes + +- 22864b7: Added an `apis` option to `createExtensionTester`, `renderInTestApp`, and `renderTestApp` to override APIs when testing extensions. Use the `mockApis` helpers to create mock implementations: + + ```typescript + import { identityApiRef } from '@backstage/frontend-plugin-api'; + import { mockApis } from '@backstage/frontend-test-utils'; + + // Override APIs in createExtensionTester + const tester = createExtensionTester(myExtension, { + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + + // Override APIs in renderInTestApp + renderInTestApp(, { + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + + // Override APIs in renderTestApp + renderTestApp({ + extensions: [myExtension], + apis: [ + [ + identityApiRef, + mockApis.identity({ userEntityRef: 'user:default/guest' }), + ], + ], + }); + ``` + +- 15ed3f9: Added `snapshot()` method to `ExtensionTester`, which returns a tree-shaped representation of the resolved extension hierarchy. Convenient to use with `toMatchInlineSnapshot()`. +- 013ec22: Added `mountedRoutes` option to `renderTestApp` for binding route refs to paths, matching the existing option in `renderInTestApp`: + + ```typescript + renderTestApp({ + extensions: [...], + mountedRoutes: { + '/my-path': myRouteRef, + }, + }); + ``` + +- d7dd5bd: Fixed Router deprecation warning and switched to using new `RouterBlueprint` from `@backstage/plugin-app-api`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/frontend-app-api@0.15.0 + - @backstage/core-app-api@1.19.5 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-app@0.4.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/test-utils@1.7.15 + - @backstage/plugin-permission-common@0.9.6 + ## 0.5.0-next.2 ### Minor Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 3e2194704c..13de1ef239 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.5.0-next.2", + "version": "0.5.1-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 748eee15d3..6d9f8964b1 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -46,10 +46,9 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageApi as StorageApi_2 } from '@backstage/frontend-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; -import { TranslationApi } from '@backstage/core-plugin-api/alpha'; -import { TranslationApi as TranslationApi_2 } from '@backstage/frontend-plugin-api'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -import { TranslationSnapshot } from '@backstage/core-plugin-api/alpha'; +import { TranslationApi } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { TranslationSnapshot } from '@backstage/frontend-plugin-api'; import { withLogCollector } from '@backstage/test-utils'; // @public @@ -87,7 +86,7 @@ export function createExtensionTester< }, ): ExtensionTester>; -// @public +// @public @deprecated export type ErrorWithContext = { error: ErrorApiError; context?: ErrorApiErrorContext; @@ -144,7 +143,7 @@ export class ExtensionTester { snapshot(): ExtensionSnapshotNode; } -// @public +// @public @deprecated export class MockAlertApi implements AlertApi { // (undocumented) alert$(): Observable; @@ -158,7 +157,7 @@ export class MockAlertApi implements AlertApi { ): Promise; } -// @public +// @public @deprecated export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) captureEvent(event: AnalyticsEvent): void; @@ -201,18 +200,18 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - export function error( - options?: MockErrorApiOptions, - ): MockErrorApi & MockWithApiFactory; + export function error(options?: { + collect?: boolean; + }): MockErrorApi & MockWithApiFactory; export namespace error { const // (undocumented) mock: ( partialImpl?: Partial | undefined, ) => ApiMock; } - export function featureFlags( - options?: MockFeatureFlagsApiOptions, - ): MockWithApiFactory; + export function featureFlags(options?: { + initialStates?: Record; + }): MockWithApiFactory; export namespace featureFlags { const mock: ( partialImpl?: Partial | undefined, @@ -265,17 +264,17 @@ export namespace mockApis { ) => ApiMock; } export function translation(): MockTranslationApi & - MockWithApiFactory; + MockWithApiFactory; export namespace translation { const mock: ( - partialImpl?: Partial | undefined, - ) => ApiMock; + partialImpl?: Partial | undefined, + ) => ApiMock; } } -// @public +// @public @deprecated export class MockConfigApi implements ConfigApi { - constructor({ data }: { data: JsonObject }); + constructor(input: { data: JsonObject }); get(key?: string): T; getBoolean(key: string): boolean; getConfig(key: string): Config; @@ -294,28 +293,37 @@ export class MockConfigApi implements ConfigApi { keys(): string[]; } -// @public +// @public @deprecated export class MockErrorApi implements ErrorApi { - constructor(options?: MockErrorApiOptions); + constructor(options?: { collect?: boolean }); // (undocumented) error$(): Observable<{ error: ErrorApiError; context?: ErrorApiErrorContext; }>; // (undocumented) - getErrors(): ErrorWithContext[]; + getErrors(): { + error: ErrorApiError; + context?: ErrorApiErrorContext; + }[]; // (undocumented) post(error: ErrorApiError, context?: ErrorApiErrorContext): void; // (undocumented) - waitForError(pattern: RegExp, timeoutMs?: number): Promise; + waitForError( + pattern: RegExp, + timeoutMs?: number, + ): Promise<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>; } -// @public +// @public @deprecated export type MockErrorApiOptions = { collect?: boolean; }; -// @public +// @public @deprecated export class MockFeatureFlagsApi implements FeatureFlagsApi { constructor(options?: MockFeatureFlagsApiOptions); clearState(): void; @@ -331,12 +339,12 @@ export class MockFeatureFlagsApi implements FeatureFlagsApi { setState(states: Record): void; } -// @public +// @public @deprecated export interface MockFeatureFlagsApiOptions { initialStates?: Record; } -// @public +// @public @deprecated export class MockFetchApi implements FetchApi { constructor(options?: MockFetchApiOptions); get fetch(): typeof fetch; @@ -360,7 +368,7 @@ export interface MockFetchApiOptions { }; } -// @public +// @public @deprecated export class MockPermissionApi implements PermissionApi { constructor( requestHandler?: ( @@ -373,7 +381,7 @@ export class MockPermissionApi implements PermissionApi { ): Promise; } -// @public +// @public @deprecated export class MockStorageApi implements StorageApi { // (undocumented) static create(data?: JsonObject): MockStorageApi; @@ -391,7 +399,7 @@ export class MockStorageApi implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// @public +// @public @deprecated export class MockTranslationApi implements TranslationApi { // (undocumented) static create(): MockTranslationApi; @@ -444,7 +452,7 @@ export type RenderTestAppOptions = { // @public export type TestApiPair = | readonly [ApiRef, TApi extends infer TImpl ? Partial : never] - | MockWithApiFactory; + | MockWithApiFactory>; // @public export type TestApiPairs = { diff --git a/packages/frontend-test-utils/src/apis/AlertApi/MockAlertApi.ts b/packages/frontend-test-utils/src/apis/AlertApi/MockAlertApi.ts index 305a527b87..dcd39fdb78 100644 --- a/packages/frontend-test-utils/src/apis/AlertApi/MockAlertApi.ts +++ b/packages/frontend-test-utils/src/apis/AlertApi/MockAlertApi.ts @@ -23,9 +23,10 @@ import ObservableImpl from 'zen-observable'; * Mock implementation of {@link @backstage/frontend-plugin-api#AlertApi} for testing alert behavior. * * @public + * @deprecated Use `mockApis.alert()` instead. * @example * ```ts - * const alertApi = new MockAlertApi(); + * const alertApi = mockApis.alert(); * alertApi.post({ message: 'Test alert' }); * expect(alertApi.getAlerts()).toHaveLength(1); * ``` diff --git a/packages/frontend-test-utils/src/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/frontend-test-utils/src/apis/AnalyticsApi/MockAnalyticsApi.ts index f6ccf4e9d1..24117d980f 100644 --- a/packages/frontend-test-utils/src/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/frontend-test-utils/src/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -21,6 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/frontend-plugin-api'; * Use getEvents in tests to verify captured events. * * @public + * @deprecated Use `mockApis.analytics()` instead. */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; diff --git a/packages/frontend-test-utils/src/apis/ConfigApi/MockConfigApi.ts b/packages/frontend-test-utils/src/apis/ConfigApi/MockConfigApi.ts index 349eee0d3a..ea445fac19 100644 --- a/packages/frontend-test-utils/src/apis/ConfigApi/MockConfigApi.ts +++ b/packages/frontend-test-utils/src/apis/ConfigApi/MockConfigApi.ts @@ -23,9 +23,10 @@ import { ConfigApi } from '@backstage/core-plugin-api'; * that can be used to mock configuration using a plain object. * * @public + * @deprecated Use `mockApis.config()` instead. * @example * ```tsx - * const mockConfig = new MockConfigApi({ + * const mockConfig = mockApis.config({ * data: { app: { baseUrl: 'https://example.com' } }, * }); * diff --git a/packages/frontend-test-utils/src/apis/ErrorApi/MockErrorApi.ts b/packages/frontend-test-utils/src/apis/ErrorApi/MockErrorApi.ts index c7c8556355..0b627d59e4 100644 --- a/packages/frontend-test-utils/src/apis/ErrorApi/MockErrorApi.ts +++ b/packages/frontend-test-utils/src/apis/ErrorApi/MockErrorApi.ts @@ -24,15 +24,16 @@ import { Observable } from '@backstage/types'; /** * Constructor arguments for {@link MockErrorApi} * @public + * @deprecated Use `mockApis.error()` instead. */ export type MockErrorApiOptions = { - // Need to be true if getErrors is used in testing. collect?: boolean; }; /** * ErrorWithContext contains error and ErrorApiErrorContext * @public + * @deprecated Use the return type of `MockErrorApi.getErrors` instead. */ export type ErrorWithContext = { error: ErrorApiError; @@ -41,7 +42,10 @@ export type ErrorWithContext = { type Waiter = { pattern: RegExp; - resolve: (err: ErrorWithContext) => void; + resolve: (err: { + error: ErrorApiError; + context?: ErrorApiErrorContext; + }) => void; }; const nullObservable = { @@ -56,12 +60,16 @@ const nullObservable = { * Mock implementation of the {@link core-plugin-api#ErrorApi} to be used in tests. * Includes withForError and getErrors methods for error testing. * @public + * @deprecated Use `mockApis.error()` instead. */ export class MockErrorApi implements ErrorApi { - private readonly errors = new Array(); + private readonly errors = new Array<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>(); private readonly waiters = new Set(); - constructor(private readonly options: MockErrorApiOptions = {}) {} + constructor(private readonly options: { collect?: boolean } = {}) {} post(error: ErrorApiError, context?: ErrorApiErrorContext) { if (this.options.collect) { @@ -87,15 +95,18 @@ export class MockErrorApi implements ErrorApi { return nullObservable; } - getErrors(): ErrorWithContext[] { + getErrors(): { error: ErrorApiError; context?: ErrorApiErrorContext }[] { return this.errors; } waitForError( pattern: RegExp, timeoutMs: number = 2000, - ): Promise { - return new Promise((resolve, reject) => { + ): Promise<{ error: ErrorApiError; context?: ErrorApiErrorContext }> { + return new Promise<{ + error: ErrorApiError; + context?: ErrorApiErrorContext; + }>((resolve, reject) => { setTimeout(() => { reject(new Error('Timed out waiting for error')); }, timeoutMs); diff --git a/packages/frontend-test-utils/src/apis/FeatureFlagsApi/MockFeatureFlagsApi.ts b/packages/frontend-test-utils/src/apis/FeatureFlagsApi/MockFeatureFlagsApi.ts index 5421687c65..591f6a2e95 100644 --- a/packages/frontend-test-utils/src/apis/FeatureFlagsApi/MockFeatureFlagsApi.ts +++ b/packages/frontend-test-utils/src/apis/FeatureFlagsApi/MockFeatureFlagsApi.ts @@ -25,6 +25,7 @@ import { * Options for configuring {@link MockFeatureFlagsApi}. * * @public + * @deprecated Use `mockApis.featureFlags()` instead. */ export interface MockFeatureFlagsApiOptions { /** @@ -37,10 +38,11 @@ export interface MockFeatureFlagsApiOptions { * Mock implementation of {@link @backstage/frontend-plugin-api#FeatureFlagsApi} for testing feature flag behavior. * * @public + * @deprecated Use `mockApis.featureFlags()` instead. * @example * ```ts - * const api = new MockFeatureFlagsApi({ - * initialStates: { 'my-feature': FeatureFlagState.Active } + * const api = mockApis.featureFlags({ + * initialStates: { 'my-feature': FeatureFlagState.Active }, * }); * expect(api.isActive('my-feature')).toBe(true); * ``` diff --git a/packages/frontend-test-utils/src/apis/FetchApi/MockFetchApi.ts b/packages/frontend-test-utils/src/apis/FetchApi/MockFetchApi.ts index b61ca680d1..0ea06924f0 100644 --- a/packages/frontend-test-utils/src/apis/FetchApi/MockFetchApi.ts +++ b/packages/frontend-test-utils/src/apis/FetchApi/MockFetchApi.ts @@ -89,6 +89,7 @@ export interface MockFetchApiOptions { * A test helper implementation of {@link @backstage/core-plugin-api#FetchApi}. * * @public + * @deprecated Use `mockApis.fetch()` instead. */ export class MockFetchApi implements FetchApi { private readonly implementation: FetchApi; diff --git a/packages/frontend-test-utils/src/apis/PermissionApi/MockPermissionApi.ts b/packages/frontend-test-utils/src/apis/PermissionApi/MockPermissionApi.ts index c54a3993d3..52f8fa4bb6 100644 --- a/packages/frontend-test-utils/src/apis/PermissionApi/MockPermissionApi.ts +++ b/packages/frontend-test-utils/src/apis/PermissionApi/MockPermissionApi.ts @@ -28,6 +28,7 @@ import { * request. * * @public + * @deprecated Use `mockApis.permission()` instead. */ export class MockPermissionApi implements PermissionApi { constructor( diff --git a/packages/frontend-test-utils/src/apis/StorageApi/MockStorageApi.ts b/packages/frontend-test-utils/src/apis/StorageApi/MockStorageApi.ts index b4cf762599..f7ad0fe39a 100644 --- a/packages/frontend-test-utils/src/apis/StorageApi/MockStorageApi.ts +++ b/packages/frontend-test-utils/src/apis/StorageApi/MockStorageApi.ts @@ -19,9 +19,10 @@ import { JsonObject, JsonValue, Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; /** - * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests + * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests. * * @public + * @deprecated Use `mockApis.storage()` instead. */ export class MockStorageApi implements StorageApi { private readonly namespace: string; diff --git a/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx b/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx new file mode 100644 index 0000000000..1bb5673751 --- /dev/null +++ b/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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 { createApiRef } from '@backstage/frontend-plugin-api'; +import { TestApiProvider } from './TestApiProvider'; +import { mockApis } from './mockApis'; +import { render, screen } from '@testing-library/react'; + +const xApiRef = createApiRef<{ a: string; b: number }>({ + id: 'x', +}); +const yApiRef = createApiRef({ + id: 'y', +}); + +describe('TestApiProvider', () => { + it('should provide tuple APIs and check types', () => { + render( + +
    + , + ); + }); + + it('should allow partial API implementations', () => { + render( + +
    + , + ); + }); + + it('should reject mismatched types in tuple syntax', () => { + render( + // @ts-expect-error - a should be a string, not a number + +
    + , + ); + }); + + it('should accept MockWithApiFactory entries', () => { + render( + +
    + , + ); + }); + + it('should accept a mix of tuples and MockWithApiFactory entries', () => { + render( + +
    + , + ); + }); + + it('should allow empty APIs', () => { + render( + +
    + , + ); + }); + + it('should provide APIs at runtime', async () => { + const alertApi = mockApis.alert(); + + render( + + rendered + , + ); + + expect(await screen.findByText('rendered')).toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-test-utils/src/apis/TestApiProvider.tsx b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx index 215e8c8a71..d0eca87a33 100644 --- a/packages/frontend-test-utils/src/apis/TestApiProvider.tsx +++ b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx @@ -29,7 +29,7 @@ import { */ export type TestApiPair = | readonly [ApiRef, TApi extends infer TImpl ? Partial : never] - | MockWithApiFactory; + | MockWithApiFactory>; /** * Represents an array of mock API implementation. diff --git a/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.test.tsx b/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.test.tsx index 4bee969a6d..8c9a15e677 100644 --- a/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.test.tsx +++ b/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { createTranslationRef } from '@backstage/frontend-plugin-api'; import { MockTranslationApi } from './MockTranslationApi'; describe('MockTranslationApi', () => { diff --git a/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.ts b/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.ts index d5fa29ea73..91708b0528 100644 --- a/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/frontend-test-utils/src/apis/TranslationApi/MockTranslationApi.ts @@ -18,7 +18,7 @@ import { TranslationApi, TranslationRef, TranslationSnapshot, -} from '@backstage/core-plugin-api/alpha'; +} from '@backstage/frontend-plugin-api'; import { createInstance as createI18n, type i18n as I18n } from 'i18next'; import ObservableImpl from 'zen-observable'; @@ -32,9 +32,10 @@ import { JsxInterpolator } from '../../../../core-app-api/src/apis/implementatio const DEFAULT_LANGUAGE = 'en'; /** - * Mock implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * Mock implementation of {@link @backstage/frontend-plugin-api#TranslationApi}. * * @public + * @deprecated Use `mockApis.translation()` instead. */ export class MockTranslationApi implements TranslationApi { static create() { diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 6e09833298..cee5c47635 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -28,59 +28,79 @@ export { type TestApiPairs, } from './TestApiProvider'; -/** - * Mock API classes are exported as types only to prevent direct instantiation. - * Always use the `mockApis` namespace to create mock instances (e.g., `mockApis.alert()`). - */ - /** * @public + * @deprecated Use `mockApis.alert()` instead. */ export type { MockAlertApi } from './AlertApi'; /** * @public + * @deprecated Use `mockApis.analytics()` instead. */ export type { MockAnalyticsApi } from './AnalyticsApi'; /** * @public + * @deprecated Use `mockApis.config()` instead. */ export type { MockConfigApi } from './ConfigApi'; /** * @public + * @deprecated Use `mockApis.error()` instead. */ -export type { - MockErrorApi, - MockErrorApiOptions, - ErrorWithContext, -} from './ErrorApi'; +export type { MockErrorApi } from './ErrorApi'; + +/** + * @public + * @deprecated Use `mockApis.error()` instead. + */ +export type { MockErrorApiOptions } from './ErrorApi/MockErrorApi'; + +/** + * @public + * @deprecated Use the return type of `MockErrorApi.getErrors` instead. + */ +export type { ErrorWithContext } from './ErrorApi/MockErrorApi'; + +/** + * @public + * @deprecated Use `mockApis.fetch()` instead. + */ +export type { MockFetchApi } from './FetchApi'; /** * @public */ -export type { MockFetchApi, MockFetchApiOptions } from './FetchApi'; +export type { MockFetchApiOptions } from './FetchApi/MockFetchApi'; /** * @public + * @deprecated Use `mockApis.featureFlags()` instead. */ -export type { - MockFeatureFlagsApi, - MockFeatureFlagsApiOptions, -} from './FeatureFlagsApi'; +export type { MockFeatureFlagsApi } from './FeatureFlagsApi'; /** * @public + * @deprecated Use `mockApis.featureFlags()` instead. + */ +export type { MockFeatureFlagsApiOptions } from './FeatureFlagsApi/MockFeatureFlagsApi'; + +/** + * @public + * @deprecated Use `mockApis.permission()` instead. */ export type { MockPermissionApi } from './PermissionApi'; /** * @public + * @deprecated Use `mockApis.storage()` instead. */ export type { MockStorageApi } from './StorageApi'; /** * @public + * @deprecated Use `mockApis.translation()` instead. */ export type { MockTranslationApi } from './TranslationApi'; diff --git a/packages/frontend-test-utils/src/apis/mockApis.ts b/packages/frontend-test-utils/src/apis/mockApis.ts index 9e3dce1e3a..96932318bb 100644 --- a/packages/frontend-test-utils/src/apis/mockApis.ts +++ b/packages/frontend-test-utils/src/apis/mockApis.ts @@ -30,6 +30,7 @@ import { type DiscoveryApi, type ErrorApi, type FetchApi, + type FeatureFlagState, type IdentityApi, type StorageApi, type TranslationApi, @@ -44,13 +45,10 @@ import { EvaluatePermissionRequest, } from '@backstage/plugin-permission-common'; import { MockAlertApi } from './AlertApi'; -import { - MockFeatureFlagsApi, - MockFeatureFlagsApiOptions, -} from './FeatureFlagsApi'; +import { MockFeatureFlagsApi } from './FeatureFlagsApi'; import { MockAnalyticsApi } from './AnalyticsApi'; import { MockConfigApi } from './ConfigApi'; -import { MockErrorApi, MockErrorApiOptions } from './ErrorApi'; +import { MockErrorApi } from './ErrorApi'; import { MockFetchApi, MockFetchApiOptions } from './FetchApi'; import { MockStorageApi } from './StorageApi'; import { MockPermissionApi } from './PermissionApi'; @@ -114,7 +112,6 @@ export namespace mockApis { /** * Mock helpers for {@link @backstage/frontend-plugin-api#AlertApi}. * - * @see {@link @backstage/frontend-plugin-api#mockApis.alert} * @public */ export namespace alert { @@ -145,9 +142,9 @@ export namespace mockApis { * expect(featureFlagsApi.isActive('my-feature')).toBe(true); * ``` */ - export function featureFlags( - options?: MockFeatureFlagsApiOptions, - ): MockWithApiFactory { + export function featureFlags(options?: { + initialStates?: Record; + }): MockWithApiFactory { const instance = new MockFeatureFlagsApi(options); return mockWithApiFactory( featureFlagsApiRef, @@ -157,7 +154,6 @@ export namespace mockApis { /** * Mock helpers for {@link @backstage/frontend-plugin-api#FeatureFlagsApi}. * - * @see {@link @backstage/frontend-plugin-api#mockApis.featureFlags} * @public */ export namespace featureFlags { @@ -201,7 +197,7 @@ export namespace mockApis { } /** - * Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * Fake implementation of {@link @backstage/frontend-plugin-api#TranslationApi}. * By default returns the default translation. * * @public @@ -216,14 +212,13 @@ export namespace mockApis { } /** - * Mock helpers for {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * Mock helpers for {@link @backstage/frontend-plugin-api#TranslationApi}. * - * @see {@link @backstage/frontend-plugin-api#mockApis.translation} * @public */ export namespace translation { /** - * Creates a mock of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * Creates a mock of {@link @backstage/frontend-plugin-api#TranslationApi}. * * @public */ @@ -419,9 +414,9 @@ export namespace mockApis { * * @public */ - export function error( - options?: MockErrorApiOptions, - ): MockErrorApi & MockWithApiFactory { + export function error(options?: { + collect?: boolean; + }): MockErrorApi & MockWithApiFactory { const instance = new MockErrorApi(options); return mockWithApiFactory(errorApiRef, instance) as MockErrorApi & MockWithApiFactory; diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md index 0b8e6effe1..438e48de39 100644 --- a/packages/integration-aws-node/CHANGELOG.md +++ b/packages/integration-aws-node/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/integration-aws-node +## 0.1.20 + +### Patch Changes + +- 7455dae: Use node prefix on native imports + ## 0.1.20-next.0 ### Patch Changes diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index 0bbfe9f96f..6070a3eea0 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-aws-node", - "version": "0.1.20-next.0", + "version": "0.1.20", "description": "Helpers for fetching AWS account credentials", "backstage": { "role": "node-library" diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index c21ea13e4c..f1ec0ba6dc 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -17,25 +17,27 @@ import { DefaultAwsCredentialsManager } from './DefaultAwsCredentialsManager'; import { mockClient, AwsClientStub } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -import { - STSClient, - GetCallerIdentityCommand, - AssumeRoleCommand, -} from '@aws-sdk/client-sts'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; import { Config, ConfigReader } from '@backstage/config'; -import { promises } from 'node:fs'; -import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import { + fromNodeProviderChain, + fromTemporaryCredentials, +} from '@aws-sdk/credential-providers'; +import { join } from 'node:path'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; const env = process.env; let stsMock: AwsClientStub; let config: Config; +let tmpDir: string; -jest.mock('fs', () => ({ promises: { readFile: jest.fn() } })); jest.mock('@aws-sdk/credential-providers', () => { const originalModule = jest.requireActual('@aws-sdk/credential-providers'); return { ...originalModule, fromNodeProviderChain: jest.fn(), + fromTemporaryCredentials: jest.fn(), }; }); @@ -103,49 +105,44 @@ describe('DefaultAwsCredentialsManager', () => { Account: '123456789012', }; }); - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws:iam::111111111111:role/hello', - RoleSessionName: 'backstage', - ExternalId: 'world', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_1', - SecretAccessKey: 'SECRET_ACCESS_KEY_1', - SessionToken: 'SESSION_TOKEN_1', - Expiration: new Date('2022-01-01'), - }, - }); - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws-other:iam::222222222222:role/hi', - RoleSessionName: 'backstage', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_2', - SecretAccessKey: 'SECRET_ACCESS_KEY_2', - SessionToken: 'SESSION_TOKEN_2', - Expiration: new Date('2022-01-02'), - }, - }); - - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws:iam::999999999999:role/backstage-role', - RoleSessionName: 'backstage', - ExternalId: 'my-id', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_9', - SecretAccessKey: 'SECRET_ACCESS_KEY_9', - SessionToken: 'SESSION_TOKEN_9', - Expiration: new Date('2022-01-09'), - }, - }); + // Mock fromTemporaryCredentials to return credential providers + // based on the RoleArn, instead of mocking internal nested STS clients. + const assumeRoleCredentials: Record< + string, + { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + expiration: Date; + } + > = { + 'arn:aws:iam::111111111111:role/hello': { + accessKeyId: 'ACCESS_KEY_ID_1', + secretAccessKey: 'SECRET_ACCESS_KEY_1', + sessionToken: 'SESSION_TOKEN_1', + expiration: new Date('2022-01-01'), + }, + 'arn:aws-other:iam::222222222222:role/hi': { + accessKeyId: 'ACCESS_KEY_ID_2', + secretAccessKey: 'SECRET_ACCESS_KEY_2', + sessionToken: 'SESSION_TOKEN_2', + expiration: new Date('2022-01-02'), + }, + 'arn:aws:iam::999999999999:role/backstage-role': { + accessKeyId: 'ACCESS_KEY_ID_9', + secretAccessKey: 'SECRET_ACCESS_KEY_9', + sessionToken: 'SESSION_TOKEN_9', + expiration: new Date('2022-01-09'), + }, + }; + (fromTemporaryCredentials as jest.Mock).mockImplementation(opts => { + const creds = assumeRoleCredentials[opts.params.RoleArn]; + if (!creds) { + throw new Error(`Unexpected RoleArn: ${opts.params.RoleArn}`); + } + return async () => creds; + }); const testDate = new Date('2022-01-10'); @@ -159,15 +156,24 @@ describe('DefaultAwsCredentialsManager', () => { jest.requireActual('@aws-sdk/credential-providers').fromNodeProviderChain, ); - const mockProfile = `[my-profile] - aws_access_key_id=ACCESS_KEY_ID_9 - aws_secret_access_key=SECRET_ACCESS_KEY_9 - `; - (promises.readFile as jest.Mock).mockResolvedValue(mockProfile); + // Write a temporary AWS credentials file and point the SDK at it + tmpDir = mkdtempSync(join(tmpdir(), 'aws-test-')); + const credFilePath = join(tmpDir, 'credentials'); + const configFilePath = join(tmpDir, 'config'); + writeFileSync( + credFilePath, + '[my-profile]\naws_access_key_id=ACCESS_KEY_ID_9\naws_secret_access_key=SECRET_ACCESS_KEY_9\n', + ); + writeFileSync(configFilePath, ''); + process.env.AWS_SHARED_CREDENTIALS_FILE = credFilePath; + process.env.AWS_CONFIG_FILE = configFilePath; }); afterEach(() => { process.env = env; + if (tmpDir) { + rmSync(tmpDir, { recursive: true, force: true }); + } }); describe('#getCredentialProvider', () => { @@ -187,12 +193,21 @@ describe('DefaultAwsCredentialsManager', () => { expiration: new Date('2022-01-01'), }); + expect(fromTemporaryCredentials).toHaveBeenCalledWith( + expect.objectContaining({ + params: { + RoleArn: 'arn:aws:iam::111111111111:role/hello', + RoleSessionName: 'backstage', + ExternalId: 'world', + }, + }), + ); + const awsCredentialProvider2 = await provider.getCredentialProvider({ accountId: '111111111111', }); expect(awsCredentialProvider).toBe(awsCredentialProvider2); - expect(stsMock).toHaveReceivedCommandTimes(AssumeRoleCommand, 1); }); it('retrieves assume-role creds in another partition for the given account ID', async () => { @@ -210,6 +225,19 @@ describe('DefaultAwsCredentialsManager', () => { sessionToken: 'SESSION_TOKEN_2', expiration: new Date('2022-01-02'), }); + + expect(fromTemporaryCredentials).toHaveBeenCalledWith( + expect.objectContaining({ + params: { + RoleArn: 'arn:aws-other:iam::222222222222:role/hi', + RoleSessionName: 'backstage', + ExternalId: undefined, + }, + clientConfig: expect.objectContaining({ + region: 'not-us-east-1', + }), + }), + ); }); it('retrieves assume-role creds for an account using the account defaults', async () => { @@ -227,6 +255,16 @@ describe('DefaultAwsCredentialsManager', () => { sessionToken: 'SESSION_TOKEN_9', expiration: new Date('2022-01-09'), }); + + expect(fromTemporaryCredentials).toHaveBeenCalledWith( + expect.objectContaining({ + params: { + RoleArn: 'arn:aws:iam::999999999999:role/backstage-role', + RoleSessionName: 'backstage', + ExternalId: 'my-id', + }, + }), + ); }); it('retrieves static creds for the given account ID', async () => { @@ -297,7 +335,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('555555555555'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_9', secretAccessKey: 'SECRET_ACCESS_KEY_9', }); @@ -312,7 +350,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('444444444444'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', @@ -336,7 +374,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_9', secretAccessKey: 'SECRET_ACCESS_KEY_9', }); @@ -354,7 +392,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', @@ -372,7 +410,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 61dd6e737b..507d66ce4e 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/integration-react +## 1.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 1.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 1.2.15 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/core-plugin-api@1.12.3 + ## 1.2.15-next.2 ### Patch Changes diff --git a/packages/integration-react/dev/DevPage.tsx b/packages/integration-react/dev/DevPage.tsx index d03baca1d7..1c94ef984a 100644 --- a/packages/integration-react/dev/DevPage.tsx +++ b/packages/integration-react/dev/DevPage.tsx @@ -16,7 +16,7 @@ import { ScmIntegration, ScmIntegrationsGroup } from '@backstage/integration'; import Typography from '@material-ui/core/Typography'; -import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi'; +import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi'; import { Content } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -48,10 +48,6 @@ export const DevPage = () => { Azure - - Bitbucket - - Bitbucket Cloud diff --git a/packages/integration-react/dev/index.tsx b/packages/integration-react/dev/index.tsx index 65486d1f06..fbff7ef0c6 100644 --- a/packages/integration-react/dev/index.tsx +++ b/packages/integration-react/dev/index.tsx @@ -15,7 +15,7 @@ */ import { createDevApp } from '@backstage/dev-utils'; import { ScmIntegrations } from '@backstage/integration'; -import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi'; +import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi'; import { DevPage } from './DevPage'; import { configApiRef, createApiFactory } from '@backstage/core-plugin-api'; diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 507fc4cdb5..ba38b2fc33 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.2.15-next.2", + "version": "1.2.16-next.1", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts index 55acd29eec..0a1f1b1b8f 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts @@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => { it('should be instantiated', () => { const i = ScmIntegrationsApi.fromConfig(new ConfigReader({})); - expect(i.list().length).toBe(8); // The default ones + expect(i.list().length).toBe(7); // The default ones }); }); diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 9d4576b3c9..f2b492d95f 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/integration +## 2.0.0-next.2 + +### Patch Changes + +- 1513a0b: Fixed a security vulnerability where path traversal sequences in SCM URLs could be used to access unintended API endpoints using server-side integration credentials. + +## 2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code: + + - For Azure DevOps, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead. + - For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead. + - For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead. + - For GitHub, the `getGitHubRequestOptions` function has been removed. + +### Patch Changes + +- 993a598: Fixed Azure integration config schema visibility annotations to use per-field `@visibility secret` instead of `@deepVisibility secret` on parent objects, so that non-secret fields like `clientId`, `tenantId`, `organizations`, and `managedIdentityClientId` are no longer incorrectly marked as secret. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.21.0-next.0 + +### Minor Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.20.0 + +### Minor Changes + +- 6999f6d: The AzureUrl class in the @backstage/integration package is now able to process BOTH git branches and git tags. Initially this class only processed git branches and threw an error when non-branch Azure URLs were passed in. + +### Patch Changes + +- cc6206e: Added support for `{org}.visualstudio.com` domains used by Azure DevOps +- 7455dae: Use node prefix on native imports + ## 1.20.0-next.2 ### Patch Changes diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 4278a8fcfe..8ea8dc2caf 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -27,40 +27,20 @@ export interface Config { * @visibility frontend */ host: string; - /** - * Token used to authenticate requests. - * @visibility secret - * @deprecated Use `credentials` instead. - */ - token?: string; - - /** - * The credential to use for requests. - * - * If no credential is specified anonymous access is used. - * - * @deepVisibility secret - * @deprecated Use `credentials` instead. - */ - credential?: { - clientId?: string; - clientSecret?: string; - tenantId?: string; - personalAccessToken?: string; - }; /** * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. * If no organization matches the first credential without an organization is used. * * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. - * @deepVisibility secret */ credentials?: { organizations?: string[]; clientId?: string; + /** @visibility secret */ clientSecret?: string; tenantId?: string; + /** @visibility secret */ personalAccessToken?: string; managedIdentityClientId?: string; }[]; @@ -111,7 +91,6 @@ export interface Config { endpoint?: string; /** * Optional credential to use for Azure Active Directory authentication. - * @deepVisibility secret */ aadCredential?: { /** @@ -126,48 +105,12 @@ export interface Config { /** * The client secret for the Azure AD application. + * @visibility secret */ clientSecret: string; }; }>; - /** - * Integration configuration for Bitbucket - * @deprecated replaced by bitbucketCloud and bitbucketServer - */ - bitbucket?: Array<{ - /** - * The hostname of the given Bitbucket instance - * @visibility frontend - */ - host: string; - /** - * Token used to authenticate requests. - * @visibility secret - */ - token?: string; - /** - * The base url for the Bitbucket API, for example https://api.bitbucket.org/2.0 - * @visibility frontend - */ - apiBaseUrl?: string; - /** - * The username to use for authenticated requests. - * @visibility secret - */ - username?: string; - /** - * Bitbucket app password used to authenticate requests. - * @visibility secret - */ - appPassword?: string; - /** - * PGP signing key for signing commits. - * @visibility secret - */ - commitSigningKey?: string; - }>; - /** Integration configuration for Bitbucket Cloud */ bitbucketCloud?: Array<{ /** @@ -386,6 +329,28 @@ export interface Config { * @visibility secret */ commitSigningKey?: string; + + /** + * Retry configuration for requests. + * @visibility frontend + */ + retry?: { + /** + * Maximum number of retries for failed requests. + * @visibility frontend + */ + maxRetries?: number; + /** + * HTTP status codes that should trigger a retry. + * @visibility frontend + */ + retryStatusCodes?: number[]; + /** + * Maximum number of API requests allowed per minute. Set to -1 to disable rate limiting. + * @visibility frontend + */ + maxApiRequestsPerMinute?: number; + }; }>; /** Integration configuration for Google Cloud Storage */ diff --git a/packages/integration/package.json b/packages/integration/package.json index 24eb80018f..578e51f28f 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.20.0-next.2", + "version": "2.0.0-next.2", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" @@ -46,7 +46,8 @@ "cross-fetch": "^4.0.0", "git-url-parse": "^15.0.0", "lodash": "^4.17.21", - "luxon": "^3.0.0" + "luxon": "^3.0.0", + "p-throttle": "^4.1.1" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/integration/report.api.md b/packages/integration/report.api.md index abcdb8898b..df1c7e9112 100644 --- a/packages/integration/report.api.md +++ b/packages/integration/report.api.md @@ -201,8 +201,6 @@ export class AzureIntegration implements ScmIntegration { // @public export type AzureIntegrationConfig = { host: string; - token?: string; - credential?: AzureDevOpsCredential; credentials?: AzureDevOpsCredential[]; commitSigningKey?: string; }; @@ -255,37 +253,6 @@ export type BitbucketCloudIntegrationConfig = { commitSigningKey?: string; }; -// @public @deprecated -export class BitbucketIntegration implements ScmIntegration { - constructor(integrationConfig: BitbucketIntegrationConfig); - // (undocumented) - get config(): BitbucketIntegrationConfig; - // (undocumented) - static factory: ScmIntegrationsFactory; - // (undocumented) - resolveEditUrl(url: string): string; - // (undocumented) - resolveUrl(options: { - url: string; - base: string; - lineNumber?: number; - }): string; - // (undocumented) - get title(): string; - // (undocumented) - get type(): string; -} - -// @public @deprecated -export type BitbucketIntegrationConfig = { - host: string; - apiBaseUrl: string; - token?: string; - username?: string; - appPassword?: string; - commitSigningKey?: string; -}; - // @public export class BitbucketServerIntegration implements ScmIntegration { constructor(integrationConfig: BitbucketServerIntegrationConfig); @@ -317,14 +284,6 @@ export type BitbucketServerIntegrationConfig = { commitSigningKey?: string; }; -// @public @deprecated -export function buildGerritGitilesArchiveUrl( - config: GerritIntegrationConfig, - project: string, - branch: string, - filePath: string, -): string; - // @public export function buildGerritGitilesArchiveUrlFromLocation( config: GerritIntegrationConfig, @@ -426,14 +385,6 @@ export function getAzureDownloadUrl(url: string): string; // @public export function getAzureFileFetchUrl(url: string): string; -// @public @deprecated -export function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ - headers: Record; -}>; - // @public export function getBitbucketCloudDefaultBranch( url: string, @@ -465,31 +416,6 @@ export function getBitbucketCloudRequestOptions( headers: Record; }>; -// @public @deprecated -export function getBitbucketDefaultBranch( - url: string, - config: BitbucketIntegrationConfig, -): Promise; - -// @public @deprecated -export function getBitbucketDownloadUrl( - url: string, - config: BitbucketIntegrationConfig, -): Promise; - -// @public @deprecated -export function getBitbucketFileFetchUrl( - url: string, - config: BitbucketIntegrationConfig, -): string; - -// @public @deprecated -export function getBitbucketRequestOptions( - config: BitbucketIntegrationConfig, -): { - headers: Record; -}; - // @public export function getBitbucketServerDefaultBranch( url: string, @@ -579,14 +505,6 @@ export function getGithubFileFetchUrl( credentials: GithubCredentials, ): string; -// @public @deprecated -export function getGitHubRequestOptions( - config: GithubIntegrationConfig, - credentials: GithubCredentials, -): { - headers: Record; -}; - // @public export function getGitilesAuthenticationUrl( config: GerritIntegrationConfig, @@ -596,7 +514,7 @@ export function getGitilesAuthenticationUrl( export function getGitLabFileFetchUrl( url: string, config: GitLabIntegrationConfig, - token?: string, + _token?: string, ): Promise; // @public @@ -759,6 +677,8 @@ export class GitLabIntegration implements ScmIntegration { // (undocumented) static factory: ScmIntegrationsFactory; // (undocumented) + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + // (undocumented) resolveEditUrl(url: string): string; // (undocumented) resolveUrl(options: { @@ -779,6 +699,11 @@ export type GitLabIntegrationConfig = { token?: string; baseUrl: string; commitSigningKey?: string; + retry?: { + maxRetries?: number; + retryStatusCodes?: number[]; + maxApiRequestsPerMinute?: number; + }; }; // @public @@ -847,8 +772,6 @@ export interface IntegrationsByType { azure: ScmIntegrationsGroup; // (undocumented) azureBlobStorage: ScmIntegrationsGroup; - // @deprecated (undocumented) - bitbucket: ScmIntegrationsGroup; // (undocumented) bitbucketCloud: ScmIntegrationsGroup; // (undocumented) @@ -867,16 +790,6 @@ export interface IntegrationsByType { harness: ScmIntegrationsGroup; } -// @public @deprecated -export function parseGerritGitilesUrl( - config: GerritIntegrationConfig, - url: string, -): { - branch: string; - filePath: string; - project: string; -}; - // @public export function parseGerritJsonResponse(response: Response): Promise; @@ -982,16 +895,6 @@ export function readBitbucketCloudIntegrationConfigs( configs: Config[], ): BitbucketCloudIntegrationConfig[]; -// @public @deprecated -export function readBitbucketIntegrationConfig( - config: Config, -): BitbucketIntegrationConfig; - -// @public @deprecated -export function readBitbucketIntegrationConfigs( - configs: Config[], -): BitbucketIntegrationConfig[]; - // @public export function readBitbucketServerIntegrationConfig( config: Config, @@ -1078,8 +981,6 @@ export interface ScmIntegrationRegistry azure: ScmIntegrationsGroup; // (undocumented) azureBlobStorage: ScmIntegrationsGroup; - // @deprecated (undocumented) - bitbucket: ScmIntegrationsGroup; // (undocumented) bitbucketCloud: ScmIntegrationsGroup; // (undocumented) @@ -1113,8 +1014,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { get azure(): ScmIntegrationsGroup; // (undocumented) get azureBlobStorage(): ScmIntegrationsGroup; - // @deprecated (undocumented) - get bitbucket(): ScmIntegrationsGroup; // (undocumented) get bitbucketCloud(): ScmIntegrationsGroup; // (undocumented) diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index 7f75ab44fc..254d774a80 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -21,8 +21,6 @@ import { BitbucketCloudIntegration, BitbucketCloudIntegrationConfig, } from './bitbucketCloud'; -import { BitbucketIntegrationConfig } from './bitbucket'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration, BitbucketServerIntegrationConfig, @@ -62,10 +60,6 @@ describe('ScmIntegrations', () => { host: 'azureblobstorage.local', } as AzureBlobStorageIntegrationConfig); - const bitbucket = new BitbucketIntegration({ - host: 'bitbucket.local', - } as BitbucketIntegrationConfig); - const bitbucketCloud = new BitbucketCloudIntegration({ host: 'bitbucket.org', } as BitbucketCloudIntegrationConfig); @@ -103,7 +97,6 @@ describe('ScmIntegrations', () => { awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host), azure: basicIntegrations([azure], item => item.config.host), azureBlobStorage: basicIntegrations([azureBlob], item => item.config.host), - bitbucket: basicIntegrations([bitbucket], item => item.config.host), bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title), bitbucketServer: basicIntegrations( [bitbucketServer], @@ -126,7 +119,6 @@ describe('ScmIntegrations', () => { expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe( azureBlob, ); - expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket); expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe( bitbucketCloud, ); @@ -147,7 +139,6 @@ describe('ScmIntegrations', () => { awsCodeCommit, azure, azureBlob, - bitbucket, bitbucketCloud, bitbucketServer, gerrit, @@ -166,7 +157,6 @@ describe('ScmIntegrations', () => { expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe( azureBlob, ); - expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket); expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud); expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer); expect(i.byUrl('https://gerrit.local')).toBe(gerrit); @@ -179,7 +169,6 @@ describe('ScmIntegrations', () => { expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit); expect(i.byHost('azure.local')).toBe(azure); expect(i.byHost('azureblobstorage.local')).toBe(azureBlob); - expect(i.byHost('bitbucket.local')).toBe(bitbucket); expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud); expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer); expect(i.byHost('gerrit.local')).toBe(gerrit); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 573975da0e..26a278fdbf 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration'; import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GithubIntegration } from './github/GithubIntegration'; @@ -42,10 +41,6 @@ export interface IntegrationsByType { awsCodeCommit: ScmIntegrationsGroup; azureBlobStorage: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; - /** - * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` - */ - bitbucket: ScmIntegrationsGroup; bitbucketCloud: ScmIntegrationsGroup; bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; @@ -70,7 +65,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { awsCodeCommit: AwsCodeCommitIntegration.factory({ config }), azureBlobStorage: AzureBlobStorageIntergation.factory({ config }), azure: AzureIntegration.factory({ config }), - bitbucket: BitbucketIntegration.factory({ config }), bitbucketCloud: BitbucketCloudIntegration.factory({ config }), bitbucketServer: BitbucketServerIntegration.factory({ config }), gerrit: GerritIntegration.factory({ config }), @@ -102,13 +96,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { return this.byType.azure; } - /** - * @deprecated in favor of `bitbucketCloud()` and `bitbucketServer()` - */ - get bitbucket(): ScmIntegrationsGroup { - return this.byType.bitbucket; - } - get bitbucketCloud(): ScmIntegrationsGroup { return this.byType.bitbucketCloud; } @@ -148,20 +135,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry { } byUrl(url: string | URL): ScmIntegration | undefined { - let candidates = Object.values(this.byType) + const candidates = Object.values(this.byType) .map(i => i.byUrl(url)) .filter(Boolean); - // Do not return deprecated integrations if there are other options - if (candidates.length > 1) { - const filteredCandidates = candidates.filter( - x => !(x instanceof BitbucketIntegration), - ); - if (filteredCandidates.length !== 0) { - candidates = filteredCandidates; - } - } - return candidates[0]; } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index c30283f432..e6a903814a 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -25,7 +25,7 @@ describe('AzureIntegration', () => { azure: [ { host: 'h.com', - token: 'token', + credentials: [{ personalAccessToken: 'token' }], }, ], }, diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index c1ddf1590f..cd2d916837 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -276,50 +276,6 @@ describe('readAzureIntegrationConfig', () => { expect(output).toEqual({ host: 'dev.azure.com' }); }); - it('maps deprecated token to credentials', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'dev.azure.com', - token: 't', - }), - ); - - expect(output).toEqual({ - host: 'dev.azure.com', - credentials: [ - { - kind: 'PersonalAccessToken', - personalAccessToken: 't', - }, - ], - }); - }); - - it('maps deprecated credential to credentials', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenantId', - }, - }), - ); - - expect(output).toEqual({ - host: 'dev.azure.com', - credentials: [ - { - kind: 'ClientSecret', - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenantId', - }, - ], - }); - }); - it('rejects config when host is not valid', () => { expect(() => readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })), diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 09b97755ec..3d6244d69a 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -32,24 +32,6 @@ export type AzureIntegrationConfig = { */ host: string; - /** - * The authorization token to use for requests. - * - * If no token is specified, anonymous access is used. - * - * @deprecated Use `credentials` instead. - */ - token?: string; - - /** - * The credential to use for requests. - * - * If no credential is specified anonymous access is used. - * - * @deprecated Use `credentials` instead. - */ - credential?: AzureDevOpsCredential; - /** * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. * If not organization matches the first credential without an organization is used. @@ -236,9 +218,17 @@ function asAzureDevOpsCredential( export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig { + deprecatedConfigCheck(config); + const host = config.getOptionalString('host') ?? AZURE_HOST; - let credentialConfigs = config + if (!isValidHost(host)) { + throw new Error( + `Invalid Azure integration config, '${host}' is not a valid host`, + ); + } + + const credentialConfigs = config .getOptionalConfigArray('credentials') ?.map(credential => { const result: Partial = { @@ -257,54 +247,6 @@ export function readAzureIntegrationConfig( return result; }); - const token = config.getOptionalString('token')?.trim(); - - if ( - config.getOptional('credential') !== undefined && - config.getOptional('credentials') !== undefined - ) { - throw new Error( - `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`, - ); - } - - if ( - config.getOptional('token') !== undefined && - config.getOptional('credentials') !== undefined - ) { - throw new Error( - `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`, - ); - } - - if (token !== undefined) { - const mapped = [{ personalAccessToken: token }]; - credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; - } - - if (config.getOptional('credential') !== undefined) { - const mapped = [ - { - organizations: config.getOptionalStringArray( - 'credential.organizations', - ), - token: config.getOptionalString('credential.token')?.trim(), - tenantId: config.getOptionalString('credential.tenantId'), - clientId: config.getOptionalString('credential.clientId'), - clientSecret: config - .getOptionalString('credential.clientSecret') - ?.trim(), - }, - ]; - credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; - } - - if (!isValidHost(host)) { - throw new Error( - `Invalid Azure integration config, '${host}' is not a valid host`, - ); - } - let credentials: AzureDevOpsCredential[] | undefined = undefined; if (credentialConfigs !== undefined) { const errors = credentialConfigs @@ -415,3 +357,19 @@ export function readAzureIntegrationConfigs( return result; } + +/** + * These config sections have been removed but to ensure they + * don't leak sensitive tokens we have this check in place + * to throw an error if found + * + * @internal + * @deprecated To be removed at a later date + */ +function deprecatedConfigCheck(config: Config) { + if (config.getOptional('credential') || config.getOptional('token')) { + throw new Error( + `Invalid Azure integration config, 'credential' and 'token' have been removed. Use 'credentials' instead.`, + ); + } +} diff --git a/packages/integration/src/azure/deprecated.test.ts b/packages/integration/src/azure/deprecated.test.ts deleted file mode 100644 index 4411ef5f2f..0000000000 --- a/packages/integration/src/azure/deprecated.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2023 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 { getAzureRequestOptions } from './deprecated'; -import { DateTime } from 'luxon'; -import { - AccessToken, - ClientSecretCredential, - ManagedIdentityCredential, -} from '@azure/identity'; - -jest.mock('@azure/identity'); - -const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< - typeof ClientSecretCredential ->; - -const MockedManagedIdentityCredential = - ManagedIdentityCredential as jest.MockedClass< - typeof ManagedIdentityCredential - >; - -describe('azure core', () => { - beforeEach(() => { - jest.resetAllMocks(); - - MockedClientSecretCredential.prototype.getToken.mockImplementation(() => - Promise.resolve({ - expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), - token: 'fake-client-secret-token', - } as AccessToken), - ); - MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => - Promise.resolve({ - expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), - token: 'fake-managed-identity-token', - } as AccessToken), - ); - }); - - describe('getAzureRequestOptions', () => { - it('should not add authorization header when not using token or credential', async () => { - expect(await getAzureRequestOptions({ host: '' })).toEqual( - expect.objectContaining({ - headers: expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - ); - }); - - it('should add authorization header when using a personal access token', async () => { - const pat = '0123456789'; - const encoded = Buffer.from(`:${pat}`).toString('base64'); - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'PersonalAccessToken', - personalAccessToken: pat, - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Basic ${encoded}`, - }), - }), - ); - }); - - it('should add authorization header when using a client secret', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'ClientSecret', - clientId: 'fake-id', - clientSecret: 'fake-secret', - tenantId: 'fake-tenant', - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-client-secret-token', - }), - }), - ); - }); - - it('should add authorization header when using a managed identity', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'ManagedIdentity', - clientId: 'fake-id', - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-managed-identity-token', - }), - }), - ); - }); - }); -}); diff --git a/packages/integration/src/azure/deprecated.ts b/packages/integration/src/azure/deprecated.ts deleted file mode 100644 index fd19f3dc14..0000000000 --- a/packages/integration/src/azure/deprecated.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 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 { AzureIntegrationConfig } from './config'; -import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @param additionalHeaders - Additional headers for the request - * @public - * @deprecated Use {@link AzureDevOpsCredentialsProvider} instead. - */ -export async function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ headers: Record }> { - const headers: Record = additionalHeaders - ? { ...additionalHeaders } - : {}; - - /* - * Since we do not have a way to determine which organization the request is for, - * we will use the first credential that does not have an organization specified. - */ - const credentialConfig = config.credentials?.filter( - credential => - credential.organizations === undefined || - credential.organizations.length === 0, - )[0]; - - if (credentialConfig) { - const credentialsProvider = - CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( - credentialConfig, - ); - const credentials = await credentialsProvider.getCredentials(); - - return { - headers: { - ...credentials?.headers, - ...headers, - }, - }; - } - - return { headers }; -} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index a447c8ffd8..b04ead862f 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -38,5 +38,3 @@ export { export * from './types'; export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; - -export * from './deprecated'; diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts deleted file mode 100644 index 575e1c9b6f..0000000000 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020 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 { ConfigReader } from '@backstage/config'; -import { BitbucketIntegration } from './BitbucketIntegration'; - -describe('BitbucketIntegration', () => { - describe('factory', () => { - it('works', () => { - const integrations = BitbucketIntegration.factory({ - config: new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'h.com', - apiBaseUrl: 'a', - token: 't', - username: 'u', - appPassword: 'p', - }, - ], - }, - }), - }); - expect(integrations.list().length).toBe(2); // including default - expect(integrations.list()[0].config.host).toBe('h.com'); - expect(integrations.list()[1].config.host).toBe('bitbucket.org'); - }); - - it('falls back to bitbucketCloud+bitbucketServer', () => { - const integrations = BitbucketIntegration.factory({ - config: new ConfigReader({ - integrations: { - bitbucketCloud: [ - { - username: 'u', - appPassword: 'p', - }, - ], - bitbucketServer: [ - { - host: 'h.com', - apiBaseUrl: 'a', - token: 't', - }, - ], - }, - }), - }); - expect(integrations.list().length).toBe(2); // including default - expect(integrations.list()[0].config.host).toBe('bitbucket.org'); - expect(integrations.list()[1].config.host).toBe('h.com'); - }); - }); - - it('returns the basics', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - expect(integration.type).toBe('bitbucket'); - expect(integration.title).toBe('h.com'); - }); - - it('resolves url line number correctly for Bitbucket Cloud', () => { - const integration = new BitbucketIntegration({ - host: 'bitbucket.org', - } as any); - - expect( - integration.resolveUrl({ - url: './a.yaml', - base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - lineNumber: 14, - }), - ).toBe( - 'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14', - ); - }); - - it('resolves url line number correctly for Bitbucket Server', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - - expect( - integration.resolveUrl({ - url: './a.yaml', - base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - lineNumber: 14, - }), - ).toBe('https://bitbucket.org/my-owner/my-project/src/master/a.yaml#14'); - }); - - it('resolve edit URL', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - - expect( - integration.resolveEditUrl( - 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - ), - ).toBe( - 'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master', - ); - }); -}); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts deleted file mode 100644 index d92532b54a..0000000000 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 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 parseGitUrl from 'git-url-parse'; -import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; -import { ScmIntegration, ScmIntegrationsFactory } from '../types'; -import { - BitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; - -/** - * A Bitbucket based integration. - * - * @public - * @deprecated replaced by the integrations bitbucketCloud and bitbucketServer. - */ -export class BitbucketIntegration implements ScmIntegration { - static factory: ScmIntegrationsFactory = ({ - config, - }) => { - const configs = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [ - // if integrations.bitbucket was not used assume the use was migrated to the new configs - // and backport for the deprecated integration to be usable for other parts of the system - // until these got migrated - ...(config.getOptionalConfigArray('integrations.bitbucketCloud') ?? []), - ...(config.getOptionalConfigArray('integrations.bitbucketServer') ?? - []), - ], - ); - return basicIntegrations( - configs.map(c => new BitbucketIntegration(c)), - i => i.config.host, - ); - }; - - constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {} - - get type(): string { - return 'bitbucket'; - } - - get title(): string { - return this.integrationConfig.host; - } - - get config(): BitbucketIntegrationConfig { - return this.integrationConfig; - } - - resolveUrl(options: { - url: string; - base: string; - lineNumber?: number; - }): string { - const resolved = defaultScmResolveUrl(options); - if (!options.lineNumber) { - return resolved; - } - - const url = new URL(resolved); - - if (this.integrationConfig.host === 'bitbucket.org') { - // Bitbucket Cloud uses the syntax #lines-{start}[:{end}][,...] - url.hash = `lines-${options.lineNumber}`; - } else { - // Bitbucket Server uses the syntax #{start}[-{end}][,...] - url.hash = `${options.lineNumber}`; - } - - return url.toString(); - } - - resolveEditUrl(url: string): string { - const urlData = parseGitUrl(url); - const editUrl = new URL(url); - - editUrl.searchParams.set('mode', 'edit'); - // TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it - // but this is taken over from the initial implementation. - editUrl.searchParams.set('spa', '0'); - editUrl.searchParams.set('at', urlData.ref); - return editUrl.toString(); - } -} diff --git a/packages/integration/src/bitbucket/config.test.ts b/packages/integration/src/bitbucket/config.test.ts deleted file mode 100644 index 8dd20cade7..0000000000 --- a/packages/integration/src/bitbucket/config.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2020 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 { Config, ConfigReader } from '@backstage/config'; -import { loadConfigSchema } from '@backstage/config-loader'; -import { - BitbucketIntegrationConfig, - readBitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; - -describe('readBitbucketIntegrationConfig', () => { - function buildConfig(data: Partial): Config { - return new ConfigReader(data); - } - - async function buildFrontendConfig( - data: Partial, - ): Promise { - const fullSchema = await loadConfigSchema({ - dependencies: ['@backstage/integration'], - }); - const serializedSchema = fullSchema.serialize() as { - schemas: { value: { properties?: { integrations?: object } } }[]; - }; - const schema = await loadConfigSchema({ - serialized: { - ...serializedSchema, // only include schemas that apply to integrations - schemas: serializedSchema.schemas.filter( - s => s.value?.properties?.integrations, - ), - }, - }); - const processed = schema.process( - [{ data: { integrations: { bitbucket: [data] } }, context: 'app' }], - { visibility: ['frontend'] }, - ); - return new ConfigReader( - (processed[0].data as any).integrations.bitbucket[0], - ); - } - - it('reads all values', () => { - const output = readBitbucketIntegrationConfig( - buildConfig({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't\n\n\n', - username: 'u', - appPassword: '\n\n\np', - }), - ); - expect(output).toEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }); - }); - - it('inserts the defaults if missing', () => { - const output = readBitbucketIntegrationConfig(buildConfig({})); - expect(output).toEqual( - expect.objectContaining({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }), - ); - }); - - it('rejects funky configs', () => { - const valid: any = { - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }; - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, host: 7 })), - ).toThrow(/host/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 7 })), - ).toThrow(/apiBaseUrl/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, token: 7 })), - ).toThrow(/token/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, username: 7 })), - ).toThrow(/username/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, appPassword: 7 })), - ).toThrow(/appPassword/); - }); - - it('works on the frontend', async () => { - expect( - readBitbucketIntegrationConfig( - await buildFrontendConfig({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }), - ), - ).toEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - }); - }); -}); - -describe('readBitbucketIntegrationConfigs', () => { - function buildConfig(data: Partial[]): Config[] { - return data.map(item => new ConfigReader(item)); - } - - it('reads all values', () => { - const output = readBitbucketIntegrationConfigs( - buildConfig([ - { - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }, - ]), - ); - expect(output).toContainEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }); - }); - - it('adds a default Bitbucket Cloud entry when missing', () => { - const output = readBitbucketIntegrationConfigs(buildConfig([])); - expect(output).toEqual([ - { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }, - ]); - }); - - it('injects the correct Bitbucket Cloud API base URL when missing', () => { - const output = readBitbucketIntegrationConfigs( - buildConfig([{ host: 'bitbucket.org' }]), - ); - expect(output).toEqual([ - { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }, - ]); - }); -}); diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts deleted file mode 100644 index 6939034e7d..0000000000 --- a/packages/integration/src/bitbucket/config.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020 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 { Config } from '@backstage/config'; -import { trimEnd } from 'lodash'; -import { isValidHost } from '../helpers'; - -const BITBUCKET_HOST = 'bitbucket.org'; -const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0'; - -/** - * The configuration parameters for a single Bitbucket API provider. - * - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export type BitbucketIntegrationConfig = { - /** - * The host of the target that this matches on, e.g. "bitbucket.org" - */ - host: string; - - /** - * The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0", - * with no trailing slash. - * - * Values omitted at the optional property at the app-config will be deduced - * from the "host" value. - */ - apiBaseUrl: string; - - /** - * The authorization token to use for requests to a Bitbucket Server provider. - * - * See https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html - * - * If no token is specified, anonymous access is used. - */ - token?: string; - - /** - * The username to use for requests to Bitbucket Cloud (bitbucket.org). - */ - username?: string; - - /** - * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords. - * - * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ - */ - appPassword?: string; - - /** - * Signing key for commits - */ - commitSigningKey?: string; -}; - -/** - * Reads a single Bitbucket integration config. - * - * @param config - The config object of a single integration - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function readBitbucketIntegrationConfig( - config: Config, -): BitbucketIntegrationConfig { - const host = config.getOptionalString('host') ?? BITBUCKET_HOST; - let apiBaseUrl = config.getOptionalString('apiBaseUrl'); - const token = config.getOptionalString('token')?.trim(); - const username = config.getOptionalString('username'); - const appPassword = config.getOptionalString('appPassword')?.trim(); - - if (!isValidHost(host)) { - throw new Error( - `Invalid Bitbucket integration config, '${host}' is not a valid host`, - ); - } - - if (apiBaseUrl) { - apiBaseUrl = trimEnd(apiBaseUrl, '/'); - } else if (host === BITBUCKET_HOST) { - apiBaseUrl = BITBUCKET_API_BASE_URL; - } else { - apiBaseUrl = `https://${host}/rest/api/1.0`; - } - - return { - host, - apiBaseUrl, - token, - username, - appPassword, - commitSigningKey: config.getOptionalString('commitSigningKey'), - }; -} - -/** - * Reads a set of Bitbucket integration configs, and inserts some defaults for - * public Bitbucket if not specified. - * - * @param configs - All of the integration config objects - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function readBitbucketIntegrationConfigs( - configs: Config[], -): BitbucketIntegrationConfig[] { - // First read all the explicit integrations - const result = configs.map(readBitbucketIntegrationConfig); - - // If no explicit bitbucket.org integration was added, put one in the list as - // a convenience - if (!result.some(c => c.host === BITBUCKET_HOST)) { - result.push({ - host: BITBUCKET_HOST, - apiBaseUrl: BITBUCKET_API_BASE_URL, - }); - } - - return result; -} diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts deleted file mode 100644 index f1b4e04f15..0000000000 --- a/packages/integration/src/bitbucket/core.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright 2020 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 { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { registerMswTestHooks } from '../helpers'; -import { BitbucketIntegrationConfig } from './config'; -import { - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, -} from './core'; - -describe('bitbucket core', () => { - const worker = setupServer(); - registerMswTestHooks(worker); - - describe('getBitbucketRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - token: 'A', - }; - const withoutToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getBitbucketRequestOptions(withToken).headers as any).Authorization, - ).toEqual('Bearer A'); - expect( - (getBitbucketRequestOptions(withoutToken).headers as any).Authorization, - ).toBeUndefined(); - }); - - it('insert basic auth when needed', () => { - const withUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - username: 'some-user', - appPassword: 'my-secret', - }; - const withoutUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getBitbucketRequestOptions(withUsernameAndPassword).headers as any) - .Authorization, - ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); - expect( - (getBitbucketRequestOptions(withoutUsernameAndPassword).headers as any) - .Authorization, - ).toBeUndefined(); - }); - }); - - describe('getBitbucketFileFetchUrl', () => { - it('rejects targets that do not look like URLs', () => { - const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getBitbucketFileFetchUrl('a/b', config)).toThrow( - /Incorrect URL: a\/b/, - ); - }); - - it('happy path for Bitbucket Cloud', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - expect( - getBitbucketFileFetchUrl( - 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', - config, - ), - ).toEqual( - 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', - ); - }); - - it('happy path for Bitbucket Server', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0', - }; - expect( - getBitbucketFileFetchUrl( - 'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml', - config, - ), - ).toEqual( - 'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=', - ); - }); - }); - - describe('getBitbucketDownloadUrl', () => { - it('add path param if a path is specified for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock&path=docs', - ); - }); - - it('does not double encode the filepath', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/%2Fdocs?at=some-branch', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=%2Fdocs', - ); - }); - - it('do not add path param if no path is specified for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse', - config, - ); - - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock', - ); - }); - - it('get by branch for Bitbucket Server', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=docs', - ); - }); - - it('do not add path param for Bitbucket Cloud', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.org/backstage/mock/src/master', - config, - ); - expect(result).toEqual( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - ); - }); - }); - - describe('getBitbucketDefaultBranch', () => { - it('return default branch for Bitbucket Cloud', async () => { - const repoInfoResponse = { - mainbranch: { - name: 'main', - }, - }; - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(repoInfoResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.org/backstage/mock/src/main', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - - it('return default branch for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - - it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(404), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - }); -}); diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts deleted file mode 100644 index 5c5533155d..0000000000 --- a/packages/integration/src/bitbucket/core.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2020 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 fetch from 'cross-fetch'; -import parseGitUrl from 'git-url-parse'; -import { BitbucketIntegrationConfig } from './config'; - -/** - * Given a URL pointing to a path on a provider, returns the default branch. - * - * @param url - A URL pointing to a path - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export async function getBitbucketDefaultBranch( - url: string, - config: BitbucketIntegrationConfig, -): Promise { - const { name: repoName, owner: project, resource } = parseGitUrl(url); - - const isHosted = resource === 'bitbucket.org'; - // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 - let branchUrl = isHosted - ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; - - let response = await fetch(branchUrl, getBitbucketRequestOptions(config)); - - if (response.status === 404 && !isHosted) { - // First try the new format, and then if it gets specifically a 404 it should try the old format - // (to support old Atlassian Bitbucket v5.11.1 format ) - branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; - response = await fetch(branchUrl, getBitbucketRequestOptions(config)); - } - - if (!response.ok) { - const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; - throw new Error(message); - } - - let defaultBranch; - if (isHosted) { - const repoInfo = await response.json(); - defaultBranch = repoInfo.mainbranch.name; - } else { - const { displayId } = await response.json(); - defaultBranch = displayId; - } - if (!defaultBranch) { - throw new Error( - `Failed to read default branch from ${branchUrl}. ` + - `Response ${response.status} ${response.json()}`, - ); - } - return defaultBranch; -} - -/** - * Given a URL pointing to a path on a provider, returns a URL that is suitable - * for downloading the subtree. - * - * @param url - A URL pointing to a path - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export async function getBitbucketDownloadUrl( - url: string, - config: BitbucketIntegrationConfig, -): Promise { - const { - name: repoName, - owner: project, - ref, - protocol, - resource, - filepath, - } = parseGitUrl(url); - - const isHosted = resource === 'bitbucket.org'; - - let branch = ref; - if (!branch) { - branch = await getBitbucketDefaultBranch(url, config); - } - // path will limit the downloaded content - // /docs will only download the docs folder and everything below it - // /docs/index.md will download the docs folder and everything below it - const path = filepath - ? `&path=${encodeURIComponent(decodeURIComponent(filepath))}` - : ''; - const archiveUrl = isHosted - ? `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`; - - return archiveUrl; -} - -/** - * Given a URL pointing to a file on a provider, returns a URL that is suitable - * for fetching the contents of the data. - * - * @remarks - * - * Converts - * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml - * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml - * - * @param url - A URL pointing to a file - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function getBitbucketFileFetchUrl( - url: string, - config: BitbucketIntegrationConfig, -): string { - try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); - if ( - !owner || - !name || - (filepathtype !== 'browse' && - filepathtype !== 'raw' && - filepathtype !== 'src') - ) { - throw new Error('Invalid Bitbucket URL or file path'); - } - - const pathWithoutSlash = filepath.replace(/^\//, ''); - - if (config.host === 'bitbucket.org') { - if (!ref) { - throw new Error('Invalid Bitbucket URL or file path'); - } - return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`; - } - return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`; - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } -} - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function getBitbucketRequestOptions( - config: BitbucketIntegrationConfig, -): { headers: Record } { - const headers: Record = {}; - - if (config.token) { - headers.Authorization = `Bearer ${config.token}`; - } else if (config.username && config.appPassword) { - const buffer = Buffer.from( - `${config.username}:${config.appPassword}`, - 'utf8', - ); - headers.Authorization = `Basic ${buffer.toString('base64')}`; - } - - return { - headers, - }; -} diff --git a/packages/integration/src/bitbucket/index.ts b/packages/integration/src/bitbucket/index.ts deleted file mode 100644 index 356e760fd8..0000000000 --- a/packages/integration/src/bitbucket/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 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 { BitbucketIntegration } from './BitbucketIntegration'; -export { - readBitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; -export type { BitbucketIntegrationConfig } from './config'; -export { - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, -} from './core'; diff --git a/packages/integration/src/bitbucketCloud/core.ts b/packages/integration/src/bitbucketCloud/core.ts index 9beb3bd748..b1f95ad2d0 100644 --- a/packages/integration/src/bitbucketCloud/core.ts +++ b/packages/integration/src/bitbucketCloud/core.ts @@ -16,6 +16,7 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { parseGitUrlSafe } from '../helpers'; import { BitbucketCloudIntegrationConfig } from './config'; import { DateTime } from 'luxon'; @@ -165,7 +166,7 @@ export async function getBitbucketCloudDownloadUrl( ref, protocol, resource, - } = parseGitUrl(url); + } = parseGitUrlSafe(url); let branch = ref; if (!branch) { @@ -193,7 +194,7 @@ export function getBitbucketCloudFileFetchUrl( config: BitbucketCloudIntegrationConfig, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) { throw new Error('Invalid Bitbucket Cloud URL or file path'); } diff --git a/packages/integration/src/bitbucketServer/core.ts b/packages/integration/src/bitbucketServer/core.ts index 8741b8f0e7..5d042d5f02 100644 --- a/packages/integration/src/bitbucketServer/core.ts +++ b/packages/integration/src/bitbucketServer/core.ts @@ -16,6 +16,7 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { parseGitUrlSafe } from '../helpers'; import { BitbucketServerIntegrationConfig } from './config'; /** @@ -74,7 +75,12 @@ export async function getBitbucketServerDownloadUrl( url: string, config: BitbucketServerIntegrationConfig, ): Promise { - const { name: repoName, owner: project, ref, filepath } = parseGitUrl(url); + const { + name: repoName, + owner: project, + ref, + filepath, + } = parseGitUrlSafe(url); let branch = ref; if (!branch) { @@ -108,7 +114,7 @@ export function getBitbucketServerFileFetchUrl( config: BitbucketServerIntegrationConfig, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if ( !owner || !name || diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index 5899196f65..b22b5f2caf 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,7 +20,6 @@ import fetch from 'cross-fetch'; import { registerMswTestHooks } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { - buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, buildGerritGitilesUrl, getGerritBranchApiUrl, @@ -28,7 +27,6 @@ import { getGerritRequestOptions, parseGerritJsonResponse, parseGitilesUrlRef, - parseGerritGitilesUrl, getGerritFileContentsApiUrl, } from './core'; @@ -36,86 +34,6 @@ describe('gerrit core', () => { const worker = setupServer(); registerMswTestHooks(worker); - describe('buildGerritGitilesArchiveUrl', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const configWithPath: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com/gerrit', - gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles', - }; - const configWithDedicatedGitiles: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com/gerrit', - gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles', - }; - it('can create an archive url for a branch', () => { - expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', - ); - - expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', - ); - }); - it('can create an archive url for a specific directory', () => { - expect( - buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('can create an authenticated url when auth is enabled', () => { - const authConfig = { - ...config, - username: 'username', - password: 'password', - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('can create an authenticated url when auth is enabled and an url-path is used', () => { - const authConfig = { - ...configWithPath, - username: 'username', - password: 'password', - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => { - const authConfig = { - ...configWithDedicatedGitiles, - username: 'username', - password: 'password', - }; - expect(() => - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toThrow( - 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', - ); - }); - it('Build a non-authenticated url when a dedicated Gitiles server is used', () => { - const authConfig = { - ...configWithDedicatedGitiles, - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - }); - describe('buildGerritGitilesArchiveUrlFromLocation', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', @@ -238,6 +156,7 @@ describe('gerrit core', () => { ).toBeUndefined(); }); }); + describe('parseGitilesUrlRef', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', @@ -360,64 +279,6 @@ describe('gerrit core', () => { }); }); }); - describe('parseGerritGitilesUrl', () => { - it('can parse a valid gitiles urls.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const { branch, filePath, project } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md', - ); - expect(project).toEqual('web/project'); - expect(branch).toEqual('master'); - expect(filePath).toEqual('README.md'); - - const { filePath: rootPath } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', - ); - expect(rootPath).toEqual('/'); - }); - it('can parse a valid authenticated gitiles url.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const { branch, filePath, project } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md', - ); - expect(project).toEqual('web/project'); - expect(branch).toEqual('master'); - expect(filePath).toEqual('README.md'); - - const { filePath: rootPath } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', - ); - expect(rootPath).toEqual('/'); - }); - it('throws on incorrect gitiles urls.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com', - }; - expect(() => - parseGerritGitilesUrl( - config, - 'https://gerrit.com/+/refs/heads/master/README.md', - ), - ).toThrow(/project/); - expect(() => - parseGerritGitilesUrl( - config, - 'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md', - ), - ).toThrow(/branch/); - }); - }); describe('getGerritBranchApiUrl', () => { it('can create an url for anonymous access.', () => { @@ -450,6 +311,45 @@ describe('gerrit core', () => { 'https://gerrit.com/a/projects/web%2Fproject/branches/master', ); }); + it('throws when ref type is not a branch (tag).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/refs/tags/v1.0.0/README.md', + ), + ).toThrow('Unsupported gitiles ref type: tag'); + }); + it('throws when ref type is not a branch (sha).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/157f862803d45b9d269f0e390f88aece1ded51e8/README.md', + ), + ).toThrow('Unsupported gitiles ref type: sha'); + }); + it('throws when ref type is not a branch (head).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/HEAD/README.md', + ), + ).toThrow('Unsupported gitiles ref type: head'); + }); }); describe('getGerritCloneRepoUrl', () => { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index ae6a5baa10..e8db1305ed 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -18,70 +18,6 @@ import { GerritIntegrationConfig } from './config'; const GERRIT_BODY_PREFIX = ")]}'"; -/** - * Parse a Gitiles URL and return branch, file path and project. - * - * @remarks - * - * Gerrit only handles code reviews so it does not have a native way to browse - * or showing the content of gits. Image if Github only had the "pull requests" - * tab. - * - * Any source code browsing is instead handled by optional services outside - * Gerrit. The url format chosen for the Gerrit url reader is the one used by - * the Gitiles project. Gerrit will work perfectly with Backstage without - * having Gitiles installed but there are some places in the Backstage GUI - * with links to the url used by the url reader. These will not work unless - * the urls point to an actual Gitiles installation. - * - * Gitiles url: - * https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} - * https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} - * - * - * @param url - An URL pointing to a file stored in git. - * @public - * @deprecated `parseGerritGitilesUrl` is deprecated. Use - * {@link parseGitilesUrlRef} instead. - */ -export function parseGerritGitilesUrl( - config: GerritIntegrationConfig, - url: string, -): { branch: string; filePath: string; project: string } { - const baseUrlParse = new URL(config.gitilesBaseUrl!); - const urlParse = new URL(url); - - // Remove the gerrit authentication prefix '/a/' from the url - // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles - // and the url provided is https://review.gerrit.com/a/plugins/gitiles/... - // remove the prefix only if the pathname start with '/a/' - const urlPath = urlParse.pathname - .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0) - .replace(baseUrlParse.pathname, ''); - - const parts = urlPath.split('/').filter(p => !!p); - - const projectEndIndex = parts.indexOf('+'); - - if (projectEndIndex <= 0) { - throw new Error(`Unable to parse project from url: ${url}`); - } - const project = trimStart(parts.slice(0, projectEndIndex).join('/'), '/'); - - const branchIndex = parts.indexOf('heads'); - if (branchIndex <= 0) { - throw new Error(`Unable to parse branch from url: ${url}`); - } - const branch = parts[branchIndex + 1]; - const filePath = parts.slice(branchIndex + 2).join('/'); - - return { - branch, - filePath: filePath === '' ? '/' : filePath, - project, - }; -} - /** * Parses Gitiles urls and returns the following: * @@ -231,30 +167,6 @@ export function buildGerritEditUrl( )}`; } -/** - * Build a Gerrit Gitiles archive url that targets a specific branch and path - * - * @param config - A Gerrit provider config. - * @param project - The name of the git project - * @param branch - The branch we will target. - * @param filePath - The absolute file path. - * @public - * @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use - * {@link buildGerritGitilesArchiveUrlFromLocation} instead. - */ -export function buildGerritGitilesArchiveUrl( - config: GerritIntegrationConfig, - project: string, - branch: string, - filePath: string, -): string { - const archiveName = - filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`; - return `${getGitilesAuthenticationUrl( - config, - )}/${project}/+archive/refs/heads/${branch}${archiveName}`; -} - /** * Build a Gerrit Gitiles archive url from a Gitiles url. * @@ -350,11 +262,15 @@ export function getGerritBranchApiUrl( config: GerritIntegrationConfig, url: string, ) { - const { branch, project } = parseGerritGitilesUrl(config, url); + const { ref, refType, project } = parseGitilesUrlRef(config, url); + + if (refType !== 'branch') { + throw new Error(`Unsupported gitiles ref type: ${refType}`); + } return `${config.baseUrl}${getAuthenticationPrefix( config, - )}projects/${encodeURIComponent(project)}/branches/${branch}`; + )}projects/${encodeURIComponent(project)}/branches/${ref}`; } /** @@ -367,7 +283,7 @@ export function getGerritCloneRepoUrl( config: GerritIntegrationConfig, url: string, ) { - const { project } = parseGerritGitilesUrl(config, url); + const { project } = parseGitilesUrlRef(config, url); return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`; } diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 534315cfba..5af75e36ab 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -19,7 +19,6 @@ export { readGerritIntegrationConfigs, } from './config'; export { - buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, getGitilesAuthenticationUrl, getGerritBranchApiUrl, @@ -28,7 +27,6 @@ export { getGerritProjectsApiUrl, getGerritRequestOptions, parseGerritJsonResponse, - parseGerritGitilesUrl, parseGitilesUrlRef, } from './core'; diff --git a/packages/integration/src/github/core.test.ts b/packages/integration/src/github/core.test.ts index 982a096b43..d129f57838 100644 --- a/packages/integration/src/github/core.test.ts +++ b/packages/integration/src/github/core.test.ts @@ -15,7 +15,7 @@ */ import { GithubIntegrationConfig } from './config'; -import { getGithubFileFetchUrl, getGitHubRequestOptions } from './core'; +import { getGithubFileFetchUrl } from './core'; import { GithubCredentials } from './types'; describe('github core', () => { @@ -35,28 +35,6 @@ describe('github core', () => { type: 'token', }; - describe('getGitHubRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: GithubIntegrationConfig = { - host: '', - rawBaseUrl: '', - token: 'A', - }; - const withoutToken: GithubIntegrationConfig = { - host: '', - rawBaseUrl: '', - }; - expect( - (getGitHubRequestOptions(withToken, appCredentials).headers as any) - .Authorization, - ).toEqual('token A'); - expect( - (getGitHubRequestOptions(withoutToken, noCredentials).headers as any) - .Authorization, - ).toBeUndefined(); - }); - }); - describe('getGithubFileFetchUrl', () => { it('rejects targets that do not look like URLs', () => { const config: GithubIntegrationConfig = { host: '', apiBaseUrl: '' }; @@ -176,5 +154,49 @@ describe('github core', () => { ), ).toEqual('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'); }); + + it('rejects URLs with encoded path traversal sequences', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }; + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fuser/repos', + config, + tokenCredentials, + ), + ).toThrow(/path traversal/); + }); + + it('rejects URLs with literal path traversal in filepath', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }; + // Literal ../ is normalized by the URL constructor before git-url-parse + // sees it, so it fails with the existing validation instead + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/../../user/repos', + config, + tokenCredentials, + ), + ).toThrow(/Incorrect URL/); + }); + + it('rejects raw endpoint URLs with path traversal', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }; + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2fuser/repos', + config, + tokenCredentials, + ), + ).toThrow(/path traversal/); + }); }); }); diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index a755f3383c..9599338497 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import parseGitUrl from 'git-url-parse'; import { GithubIntegrationConfig } from './config'; +import { parseGitUrlSafe } from '../helpers'; import { GithubCredentials } from './types'; /** @@ -39,7 +39,7 @@ export function getGithubFileFetchUrl( credentials: GithubCredentials, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if ( !owner || !name || @@ -63,30 +63,6 @@ export function getGithubFileFetchUrl( } } -/** - * Gets the request options necessary to make requests to a given provider. - * - * @deprecated This function is no longer used internally - * @param config - The relevant provider config - * @public - */ -export function getGitHubRequestOptions( - config: GithubIntegrationConfig, - credentials: GithubCredentials, -): { headers: Record } { - const headers: Record = {}; - - if (chooseEndpoint(config, credentials) === 'api') { - headers.Accept = 'application/vnd.github.v3.raw'; - } - - if (credentials.token) { - headers.Authorization = `token ${credentials.token}`; - } - - return { headers }; -} - export function chooseEndpoint( config: GithubIntegrationConfig, credentials: GithubCredentials, diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index ea6f18f2e0..450ac38a63 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -19,7 +19,7 @@ export { readGithubIntegrationConfigs, } from './config'; export type { GithubAppConfig, GithubIntegrationConfig } from './config'; -export { getGithubFileFetchUrl, getGitHubRequestOptions } from './core'; +export { getGithubFileFetchUrl } from './core'; export { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider'; export { GithubAppCredentialsMux, diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 575ce75308..7489badb8c 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -14,8 +14,20 @@ * limitations under the License. */ +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; -import { GitLabIntegration, replaceGitLabUrlType } from './GitLabIntegration'; +import { + GitLabIntegration, + replaceGitLabUrlType, + sleep, +} from './GitLabIntegration'; +import { registerMswTestHooks } from '../helpers'; + +// Mock pThrottle to make testing easier +jest.mock('p-throttle', () => { + return jest.fn(() => (fn: any) => fn); +}); describe('GitLabIntegration', () => { it('has a working factory', () => { @@ -45,7 +57,11 @@ describe('GitLabIntegration', () => { }); it('resolve edit URL', () => { - const integration = new GitLabIntegration({ host: 'h.com' } as any); + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + }); expect( integration.resolveEditUrl( @@ -53,6 +69,349 @@ describe('GitLabIntegration', () => { ), ).toBe('https://gitlab.com/my-org/my-project/-/edit/develop/README.md'); }); + + describe('fetch strategy', () => { + const worker = setupServer(); + registerMswTestHooks(worker); + + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { + jest.clearAllTimers(); + }); + + it('uses plain fetch when no throttling or retries configured', async () => { + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + }); + + let calledUrl: string | undefined; + worker.use( + rest.get('https://h.com/api/v4', (req, res, ctx) => { + calledUrl = req.url.href; + return res(ctx.status(200)); + }), + ); + + await integration.fetch('https://h.com/api/v4'); + expect(calledUrl).toBe('https://h.com/api/v4'); + }); + + it('applies retry logic when maxRetries > 0', async () => { + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + if (callCount === 1) { + return res(ctx.status(429), ctx.json({})); + } + return res(ctx.status(200), ctx.json({})); + }), + ); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxRetries: 3, + retryStatusCodes: [429], + }, + }); + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(100); + await jest.advanceTimersByTimeAsync(200); + await jest.advanceTimersByTimeAsync(400); + const response = await responsePromise; + + expect(response.status).toBe(200); + expect(callCount).toBe(2); + }); + + it('does not retry when status code is not in retryStatusCodes', async () => { + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + return res(ctx.status(404)); + }), + ); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxRetries: 3, + retryStatusCodes: [429, 500], + }, + }); + + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(1000); + const response = await responsePromise; + + expect(response.status).toBe(404); + expect(callCount).toBe(1); + }); + + it('stops retrying after maxRetries attempts', async () => { + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + return res(ctx.status(429)); + }), + ); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxRetries: 2, + retryStatusCodes: [429], + }, + }); + + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(100); + await jest.advanceTimersByTimeAsync(200); + await jest.advanceTimersByTimeAsync(400); + const response = await responsePromise; + + expect(response.status).toBe(429); + expect(callCount).toBe(3); // initial + 2 retries + }); + + it('applies throttling when limitPerMinute > 0', async () => { + const pThrottle = require('p-throttle'); + const throttleMock = jest.fn(() => (fn: any) => fn); + pThrottle.mockReturnValue(throttleMock); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxApiRequestsPerMinute: 60, + }, + }); + + await integration.fetch('https://h.com/api/v4'); + + expect(pThrottle).toHaveBeenCalledWith({ + limit: 60, + interval: 60_000, + }); + expect(throttleMock).toHaveBeenCalled(); + }); + + it('applies both throttling and retry when both are configured', async () => { + const pThrottle = require('p-throttle'); + + const throttleMock = jest.fn((fn: any) => fn); + pThrottle.mockReturnValue(throttleMock); + + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + if (callCount === 1) { + return res(ctx.status(429), ctx.json({})); + } + return res(ctx.status(200), ctx.json({})); + }), + ); + + const integration = new GitLabIntegration({ + apiBaseUrl: 'https://h.com/api/v4', + host: 'h.com', + baseUrl: 'https://h.com', + retry: { + maxRetries: 3, + retryStatusCodes: [429], + maxApiRequestsPerMinute: 60, + }, + }); + + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(100); + const response = await responsePromise; + + expect(response.status).toBe(200); + expect(pThrottle).toHaveBeenCalledWith({ + limit: 60, + interval: 60_000, + }); + expect(callCount).toBe(2); + }); + + it('retries based on configured status codes', async () => { + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + if (callCount === 1) { + return res( + ctx.status(429), + ctx.set('Retry-After', '1'), + ctx.json({}), + ); + } + return res(ctx.status(200), ctx.json({})); + }), + ); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxRetries: 3, + retryStatusCodes: [429], + }, + }); + + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(1000); + const response = await responsePromise; + + expect(response.status).toBe(200); + expect(callCount).toBe(2); + }); + + it('retries multiple times for persistent failures', async () => { + let callCount = 0; + worker.use( + rest.get('https://h.com/api/v4', (_req, res, ctx) => { + callCount += 1; + if (callCount < 3) { + return res(ctx.status(500), ctx.json({})); + } + return res(ctx.status(200), ctx.json({})); + }), + ); + + const integration = new GitLabIntegration({ + host: 'h.com', + apiBaseUrl: 'https://h.com/api/v4', + baseUrl: 'https://h.com', + retry: { + maxRetries: 3, + retryStatusCodes: [500], + }, + }); + + const responsePromise = integration.fetch('https://h.com/api/v4'); + await jest.advanceTimersByTimeAsync(100); + await jest.advanceTimersByTimeAsync(200); + await jest.advanceTimersByTimeAsync(400); + const response = await responsePromise; + + expect(response.status).toBe(200); + expect(callCount).toBe(3); + }); + }); +}); + +describe('sleep', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { + jest.clearAllTimers(); + }); + + it('should resolve after the specified duration when not aborted', async () => { + const duration = 1000; + const sleepPromise = sleep(duration, null); + + // Fast-forward timers to trigger the timeout + jest.advanceTimersByTimeAsync(duration); + + await expect(sleepPromise).resolves.toBeUndefined(); + }); + + it('should resolve immediately if abortSignal is already aborted', async () => { + const abortController = new AbortController(); + abortController.abort(); + + const sleepPromise = sleep(5000, abortController.signal); + + // Should resolve immediately without needing to advance timers + await expect(sleepPromise).resolves.toBeUndefined(); + }); + + it('should resolve when aborted during wait', async () => { + const abortController = new AbortController(); + const duration = 5000; + + const sleepPromise = sleep(duration, abortController.signal); + + // Abort the signal after starting the sleep + abortController.abort(); + + // Should resolve immediately when aborted + await expect(sleepPromise).resolves.toBeUndefined(); + }); + + it('should handle undefined abortSignal gracefully', async () => { + const duration = 500; + const sleepPromise = sleep(duration, undefined); + + // Fast-forward timers to trigger the timeout + jest.advanceTimersByTimeAsync(duration); + + await expect(sleepPromise).resolves.toBeUndefined(); + }); + + it('should clean up timeout when aborted', async () => { + const abortController = new AbortController(); + const duration = 10000; + + const sleepPromise = sleep(duration, abortController.signal); + + // Check that a timer was set + expect(jest.getTimerCount()).toBe(1); + + // Abort the signal + abortController.abort(); + + // Wait for the promise to resolve + await sleepPromise; + + // Timer should be cleaned up + expect(jest.getTimerCount()).toBe(0); + }); + + it('should clean up abort event listener when timeout completes', async () => { + const abortController = new AbortController(); + const duration = 1000; + + const sleepPromise = sleep(duration, abortController.signal); + + // Fast-forward timers to complete the timeout + jest.advanceTimersByTimeAsync(duration); + + // Wait for the promise to complete and verify it resolves properly + await expect(sleepPromise).resolves.toBeUndefined(); + + // Event listener should be cleaned up - aborting after completion should not cause issues + abortController.abort(); // This should not affect anything since the sleep is already done + + // Verify the sleep function handled cleanup properly + expect(jest.getTimerCount()).toBe(0); + }); }); describe('replaceGitLabUrlType', () => { diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 0c52799599..68afb6f63e 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitLabIntegrationConfig, readGitLabIntegrationConfigs, } from './config'; +import pThrottle from 'p-throttle'; + +type FetchFunction = typeof fetch; /** * A GitLab based integration. @@ -37,7 +39,12 @@ export class GitLabIntegration implements ScmIntegration { ); }; - constructor(private readonly integrationConfig: GitLabIntegrationConfig) {} + private readonly fetchImpl: FetchFunction; + + constructor(private readonly integrationConfig: GitLabIntegrationConfig) { + // Configure fetch strategy based on configuration + this.fetchImpl = this.createFetchStrategy(); + } get type(): string { return 'gitlab'; @@ -62,6 +69,97 @@ export class GitLabIntegration implements ScmIntegration { resolveEditUrl(url: string): string { return replaceGitLabUrlType(url, 'edit'); } + + fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return this.fetchImpl(input, init); + } + + private createFetchStrategy(): FetchFunction { + let fetchFn: FetchFunction = async (url, options) => { + return fetch(url, { ...options, mode: 'same-origin' }); + }; + + const retryConfig = this.integrationConfig.retry; + if (retryConfig) { + // Apply retry wrapper if configured + fetchFn = this.withRetry(fetchFn, retryConfig); + + // Apply throttling wrapper if configured + if ( + retryConfig.maxApiRequestsPerMinute && + retryConfig.maxApiRequestsPerMinute > 0 + ) { + fetchFn = pThrottle({ + limit: retryConfig.maxApiRequestsPerMinute, + interval: 60_000, + })(fetchFn); + } + } + + return fetchFn; + } + + private withRetry( + fetchFn: FetchFunction, + retryConfig: { maxRetries?: number; retryStatusCodes?: number[] }, + ): FetchFunction { + const maxRetries = retryConfig?.maxRetries ?? 0; + const retryStatusCodes = retryConfig?.retryStatusCodes ?? []; + if (maxRetries <= 0 || retryStatusCodes.length === 0) { + return fetchFn; + } + + return async (url, options) => { + const abortSignal = options?.signal; + let response: Response; + let attempt = 0; + for (;;) { + response = await fetchFn(url, options); + // If response is not retryable, return immediately + if (!retryStatusCodes.includes(response.status)) { + break; + } + + // If this was the last allowed attempt, return response + if (attempt++ >= maxRetries) { + break; + } + // Determine delay from Retry-After header if present, otherwise exponential backoff + const retryAfter = response.headers.get('Retry-After'); + const delay = retryAfter + ? parseInt(retryAfter, 10) * 1000 + : Math.min(100 * Math.pow(2, attempt - 1), 10000); // Exponential backoff, cap at 10 seconds + + await sleep(delay, abortSignal); + } + + return response; + }; + } +} + +export async function sleep( + durationMs: number, + abortSignal: AbortSignal | null | undefined, +): Promise { + if (abortSignal?.aborted) { + return; + } + + await new Promise(resolve => { + let timeoutHandle: NodeJS.Timeout | undefined = undefined; + + const done = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + abortSignal?.removeEventListener('abort', done); + resolve(); + }; + + timeoutHandle = setTimeout(done, durationMs); + abortSignal?.addEventListener('abort', done); + }); } /** diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index f1d17a6d7e..693e31e2e4 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -58,6 +58,11 @@ describe('readGitLabIntegrationConfig', () => { token: ' t\n', apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', + retry: { + maxRetries: 3, + maxApiRequestsPerMinute: 1000, + retryStatusCodes: [429], + }, }), ); @@ -66,6 +71,12 @@ describe('readGitLabIntegrationConfig', () => { token: 't', apiBaseUrl: 'https://a.com', baseUrl: 'https://baseurl.for.me/gitlab', + commitSigningKey: undefined, + retry: { + maxRetries: 3, + maxApiRequestsPerMinute: 1000, + retryStatusCodes: [429], + }, }); }); @@ -77,6 +88,8 @@ describe('readGitLabIntegrationConfig', () => { host: 'gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', baseUrl: 'https://gitlab.com', + commitSigningKey: undefined, + retry: undefined, }); }); @@ -89,6 +102,7 @@ describe('readGitLabIntegrationConfig', () => { host: 'gitlab.com', baseUrl: 'https://gitlab.com', apiBaseUrl: 'https://gitlab.com/api/v4', + retry: undefined, }); }); @@ -119,6 +133,9 @@ describe('readGitLabIntegrationConfig', () => { host: 'a.com', apiBaseUrl: 'https://a.com/api', baseUrl: 'https://a.com', + token: undefined, // token is filtered out on frontend + commitSigningKey: undefined, + retry: undefined, }); }); }); @@ -144,6 +161,7 @@ describe('readGitLabIntegrationConfigs', () => { token: 't', apiBaseUrl: 'https://a.com/api/v4', baseUrl: 'https://a.com', + retry: undefined, }); }); diff --git a/packages/integration/src/gitlab/config.ts b/packages/integration/src/gitlab/config.ts index 636919291c..344d054904 100644 --- a/packages/integration/src/gitlab/config.ts +++ b/packages/integration/src/gitlab/config.ts @@ -21,6 +21,32 @@ import { isValidHost, isValidUrl } from '../helpers'; const GITLAB_HOST = 'gitlab.com'; const GITLAB_API_BASE_URL = 'https://gitlab.com/api/v4'; +/** + * Reads an optional number array from config + */ +function readOptionalNumberArray( + config: Config, + key: string, +): number[] | undefined { + const value = config.getOptional(key); + if (value === undefined) { + return undefined; + } + if (!Array.isArray(value)) { + throw new Error( + `Invalid ${key} config: expected an array, got ${typeof value}`, + ); + } + return value.map((item, index) => { + if (typeof item !== 'number') { + throw new Error( + `Invalid ${key} config: all values must be numbers, got ${typeof item} at index ${index}`, + ); + } + return item; + }); +} + /** * The configuration parameters for a single GitLab integration. * @@ -59,6 +85,29 @@ export type GitLabIntegrationConfig = { * Signing key to sign commits */ commitSigningKey?: string; + + /** + * Retry configuration for failed requests. + */ + retry?: { + /** + * Maximum number of retries for failed requests + * @defaultValue 0 + */ + maxRetries?: number; + + /** + * HTTP status codes that should trigger a retry + * @defaultValue [] + */ + retryStatusCodes?: number[]; + + /** + * Rate limit for requests per minute + * @defaultValue -1 + */ + maxApiRequestsPerMinute?: number; + }; }; /** @@ -100,12 +149,25 @@ export function readGitLabIntegrationConfig( ); } + const retryConfig = config.getOptionalConfig('retry'); + + const retry = retryConfig + ? { + maxRetries: retryConfig.getOptionalNumber('maxRetries') ?? 0, + retryStatusCodes: + readOptionalNumberArray(retryConfig, 'retryStatusCodes') ?? [], + maxApiRequestsPerMinute: + retryConfig.getOptionalNumber('maxApiRequestsPerMinute') ?? -1, + } + : undefined; + return { host, token, apiBaseUrl, baseUrl, commitSigningKey: config.getOptionalString('commitSigningKey'), + retry, }; } diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index d514dba755..58fcbe34a5 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -14,29 +14,14 @@ * limitations under the License. */ -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; import { GitLabIntegrationConfig } from './config'; -import { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; - -const worker = setupServer(); +import { + getGitLabFileFetchUrl, + getGitLabRequestOptions, + extractProjectPath, +} from './core'; describe('gitlab core', () => { - beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); - afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); - - beforeEach(() => { - worker.use( - rest.get('*/api/v4/projects/group%2Fproject', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), - rest.get('*/api/v4/projects/group%2Fsubgroup%2Fproject', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), - ); - }); - const configWithNoToken: GitLabIntegrationConfig = { host: 'gitlab.com', apiBaseUrl: '', @@ -63,7 +48,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/-/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -73,7 +58,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/-/blob/branch/blob/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/blob%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/blob%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -83,7 +68,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/subgroup/project/-/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fsubgroup%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -93,7 +78,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/-/blob/branch/folder/file.yml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -103,7 +88,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/-/blob/branch/folder/file with spaces.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -114,7 +99,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.mycompany.com/group/project/-/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.mycompany.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configSelfHostedWithoutRelativePath), ).resolves.toBe(fetchUrl); @@ -124,7 +109,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.mycompany.com/group/project/-/blob/branch/folder/file with spaces.yaml'; const fetchUrl = - 'https://gitlab.mycompany.com/api/v4/projects/12345/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; + 'https://gitlab.mycompany.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configSelfHostedWithoutRelativePath), ).resolves.toBe(fetchUrl); @@ -135,7 +120,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.mycompany.com/gitlab/group/project/-/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configSelfHosteWithRelativePath), ).resolves.toBe(fetchUrl); @@ -145,7 +130,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.mycompany.com/gitlab/group/project/-/blob/branch/folder/file with spaces.yaml'; const fetchUrl = - 'https://gitlab.mycompany.com/gitlab/api/v4/projects/12345/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; + 'https://gitlab.mycompany.com/gitlab/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile%20with%20spaces.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configSelfHosteWithRelativePath), ).resolves.toBe(fetchUrl); @@ -159,7 +144,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -169,7 +154,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/subgroup/project/blob/branch/folder/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=branch'; + 'https://gitlab.com/api/v4/projects/group%2Fsubgroup%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=branch'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -179,7 +164,7 @@ describe('gitlab core', () => { const target = 'https://gitlab.com/group/project/blob/blob/folder/file.yaml'; const fetchUrl = - 'https://gitlab.com/api/v4/projects/12345/repository/files/folder%2Ffile.yaml/raw?ref=blob'; + 'https://gitlab.com/api/v4/projects/group%2Fproject/repository/files/folder%2Ffile.yaml/raw?ref=blob'; await expect( getGitLabFileFetchUrl(target, configWithNoToken), ).resolves.toBe(fetchUrl); @@ -187,8 +172,49 @@ describe('gitlab core', () => { }); }); + describe('extractProjectPath', () => { + it('extracts project path from scoped route', () => { + const target = + 'https://gitlab.com/group/project/-/blob/branch/folder/file.yaml'; + expect(extractProjectPath(target, configWithNoToken)).toBe( + 'group/project', + ); + }); + + it('extracts project path from subgroup', () => { + const target = + 'https://gitlab.com/group/subgroup/project/-/blob/branch/folder/file.yaml'; + expect(extractProjectPath(target, configWithNoToken)).toBe( + 'group/subgroup/project', + ); + }); + + it('extracts project path from unscoped route', () => { + const target = + 'https://gitlab.com/group/project/blob/branch/folder/file.yaml'; + expect(extractProjectPath(target, configWithNoToken)).toBe( + 'group/project', + ); + }); + + it('extracts project path from self-hosted gitlab with relative path', () => { + const target = + 'https://gitlab.mycompany.com/gitlab/group/project/-/blob/branch/folder/file.yaml'; + expect(extractProjectPath(target, configSelfHosteWithRelativePath)).toBe( + 'group/project', + ); + }); + + it('throws error for invalid URLs without blob path', () => { + const target = 'https://gitlab.com/some/random/endpoint'; + expect(() => extractProjectPath(target, configWithNoToken)).toThrow( + 'Failed extracting project path from /some/random/endpoint. Url path must include /blob/.', + ); + }); + }); + describe('getGitLabRequestOptions', () => { - it('should return Authorization bearer header when a token is provided', () => { + it('should return Authorization bearer header when a token is provided', async () => { const token = '1234567890'; const result = getGitLabRequestOptions( configSelfHosteWithRelativePath, @@ -202,7 +228,7 @@ describe('gitlab core', () => { }); }); - it('should return Authorization bearer header using the config token when no token is provided', () => { + it('should return Authorization bearer header using the config token when no token is provided', async () => { const result = getGitLabRequestOptions(configSelfHosteWithRelativePath); expect(result).toEqual({ diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 65360840b5..435e5a8a31 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; import { getGitLabIntegrationRelativePath, GitLabIntegrationConfig, @@ -28,22 +27,25 @@ import { * * Converts * from: https://gitlab.example.com/a/b/blob/master/c.yaml - * to: https://gitlab.com/api/v4/projects/projectId/repository/c.yaml?ref=master + * to: https://gitlab.com/api/v4/projects/a%2Fb/repository/files/c.yaml/raw?ref=master * -or- * from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath - * to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch + * to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FteamA%2FsubgroupA%2FrepoA/repository/files/filepath/raw?ref=branch * * @param url - A URL pointing to a file * @param config - The relevant provider config + * @param token - An optional auth token (not used in path extraction, kept for compatibility) * @public */ -export async function getGitLabFileFetchUrl( +export function getGitLabFileFetchUrl( url: string, config: GitLabIntegrationConfig, - token?: string, + _token?: string, ): Promise { - const projectID = await getProjectId(url, config, token); - return buildProjectUrl(url, projectID, config).toString(); + // Use project path directly instead of making an API call to get project ID + // Note: _token parameter kept for backward compatibility but not used for path extraction + const projectPath = extractProjectPath(url, config); + return Promise.resolve(buildProjectUrl(url, projectPath, config).toString()); } /** @@ -72,10 +74,10 @@ export function getGitLabRequestOptions( // Converts // from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath -// to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch +// to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FteamA%2FsubgroupA%2FrepoA/repository/files/filepath/raw?ref=branch export function buildProjectUrl( target: string, - projectID: Number, + projectPathOrID: string | Number, config: GitLabIntegrationConfig, ): URL { try { @@ -88,10 +90,12 @@ export function buildProjectUrl( const [branch, ...filePath] = branchAndFilePath.split('/'); const relativePath = getGitLabIntegrationRelativePath(config); + const projectIdentifier = encodeURIComponent(String(projectPathOrID)); + url.pathname = [ ...(relativePath ? [relativePath] : []), 'api/v4/projects', - projectID, + projectIdentifier, 'repository/files', encodeURIComponent(decodeURIComponent(filePath.join('/'))), 'raw', @@ -105,62 +109,33 @@ export function buildProjectUrl( } } -// Convert -// from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath -// to: The project ID that corresponds to the URL -export async function getProjectId( +/** + * Extracts the project path from a GitLab URL + * from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + * to: groupA/teams/teamA/subgroupA/repoA + */ +export function extractProjectPath( target: string, config: GitLabIntegrationConfig, - token?: string, -): Promise { +): string { const url = new URL(target); if (!url.pathname.includes('/blob/')) { throw new Error( - `Failed converting ${url.pathname} to a project id. Url path must include /blob/.`, + `Failed extracting project path from ${url.pathname}. Url path must include /blob/.`, ); } - try { - let repo = url.pathname.split('/-/blob/')[0].split('/blob/')[0]; + let repo = url.pathname.split('/-/blob/')[0].split('/blob/')[0]; - // Get gitlab relative path - const relativePath = getGitLabIntegrationRelativePath(config); + // Get gitlab relative path + const relativePath = getGitLabIntegrationRelativePath(config); - // Check relative path exist and replace it if it's the case. - if (relativePath) { - repo = repo.replace(relativePath, ''); - } - - // Convert - // to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo - const repoIDLookup = new URL( - `${url.origin}${relativePath}/api/v4/projects/${encodeURIComponent( - repo.replace(/^\//, ''), - )}`, - ); - - const response = await fetch( - repoIDLookup.toString(), - getGitLabRequestOptions(config, token), - ); - - const data = await response.json(); - - if (!response.ok) { - if (response.status === 401) { - throw new Error( - 'GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project', - ); - } - - throw new Error( - `GitLab Error '${data.error}', ${data.error_description}`, - ); - } - - return Number(data.id); - } catch (e) { - throw new Error(`Could not get GitLab project ID for: ${target}, ${e}`); + // Check relative path exist and replace it if it's the case. + if (relativePath) { + repo = repo.replace(relativePath, ''); } + + // Remove leading slash + return repo.replace(/^\//, ''); } diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index f1dd2e7ac8..b12236b031 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -19,6 +19,7 @@ import { basicIntegrations, defaultScmResolveUrl, isValidHost, + parseGitUrlSafe, } from './helpers'; describe('basicIntegrations', () => { @@ -317,3 +318,99 @@ describe('defaultScmResolveUrl', () => { ).toBe('https://b.com/b.yaml'); }); }); + +describe('parseGitUrlSafe', () => { + it('parses a valid GitHub blob URL', () => { + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/path/to/file.yaml', + ); + expect(result.owner).toBe('owner'); + expect(result.name).toBe('repo'); + expect(result.ref).toBe('main'); + expect(result.filepath).toBe('path/to/file.yaml'); + }); + + it('rejects URLs with encoded path traversal in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with double-encoded path traversal in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/foo%2f%2e%2e%2fbar%2f%2e%2e%2f%2e%2e%2fsecret', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with uppercase %2E encoding in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2E%2E%2F%2E%2E%2Fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with mixed-case percent encoding', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2E%2e%2Fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs where git-url-parse leaves percent-encoded traversal segments', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%252e%252e%252fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with triple-encoded path traversal', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%25252e%25252e%25252fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with literal .. in the middle of the filepath', () => { + // URL normalization resolves foo/../bar to just bar, so the filepath + // won't contain traversal. But we verify parseGitUrlSafe still handles + // the resolved path safely. + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/foo/../bar', + ); + expect(result.filepath).toBe('bar'); + }); + + it('handles literal ../ that gets normalized away by URL parsing', () => { + // Literal ../../../ gets resolved by the URL constructor before + // git-url-parse sees it. This mangles owner/name but the filepath + // is empty, so parseGitUrlSafe doesn't reject it. Downstream + // functions reject these via their own validation. + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/../../../user/repos', + ); + expect(result.filepath).toBe(''); + expect(result.owner).not.toBe('owner'); + }); + + it('allows filenames that contain dots but are not traversal', () => { + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/path/to/some..file.yaml', + ); + expect(result.filepath).toBe('path/to/some..file.yaml'); + }); + + it('allows URLs without a filepath', () => { + const result = parseGitUrlSafe('https://github.com/owner/repo'); + expect(result.owner).toBe('owner'); + expect(result.name).toBe('repo'); + expect(result.filepath).toBe(''); + }); +}); diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index f56d360325..9a7012d9ce 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -18,6 +18,38 @@ import parseGitUrl from 'git-url-parse'; import { trimEnd } from 'lodash'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; +/** + * Wraps git-url-parse and rejects URLs whose filepath contains path traversal + * segments. Without this check, a URL like + * `https://github.com/o/r/blob/main/%2e%2e%2f%2e%2e%2fuser/repos` would be + * decoded to `../../user/repos` and could escape the expected API path when + * interpolated into provider API URLs. + */ +export function parseGitUrlSafe(url: string) { + const parsed = parseGitUrl(url); + if (parsed.filepath) { + let decoded = parsed.filepath; + let previous; + do { + previous = decoded; + try { + decoded = decodeURIComponent(decoded); + } catch { + break; + } + } while (decoded !== previous); + + if ( + decoded.split('/').some(segment => segment === '..' || segment === '.') + ) { + throw new Error( + 'Invalid SCM URL: path traversal is not allowed in the URL', + ); + } + } + return parsed; +} + /** Checks whether the given argument is a valid URL hostname */ export function isValidHost(host: string): boolean { const check = new URL('http://example.com'); @@ -84,7 +116,7 @@ export function defaultScmResolveUrl(options: { if (url.startsWith('/')) { // If it is an absolute path, move relative to the repo root - const { href, filepath } = parseGitUrl(base); + const { href, filepath } = parseGitUrlSafe(base); updated = new URL(href); diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index a625f5d83c..1c0919fcbd 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -24,7 +24,6 @@ export * from './awsS3'; export * from './awsCodeCommit'; export * from './azureBlobStorage'; export * from './azure'; -export * from './bitbucket'; export * from './bitbucketCloud'; export * from './bitbucketServer'; export * from './gerrit'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 2e8111903b..6aca5b197b 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AwsCodeCommitIntegration } from './awsCodeCommit'; import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GithubIntegration } from './github/GithubIntegration'; @@ -39,10 +38,6 @@ export interface ScmIntegrationRegistry awsCodeCommit: ScmIntegrationsGroup; azureBlobStorage: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; - /** - * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` - */ - bitbucket: ScmIntegrationsGroup; bitbucketCloud: ScmIntegrationsGroup; bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; diff --git a/packages/module-federation-common/CHANGELOG.md b/packages/module-federation-common/CHANGELOG.md new file mode 100644 index 0000000000..9017b9b96d --- /dev/null +++ b/packages/module-federation-common/CHANGELOG.md @@ -0,0 +1,17 @@ +# @backstage/module-federation-common + +## 0.1.2-next.0 + +### Patch Changes + +- 0cb5646: Fixed the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.0 + +### Minor Changes + +- ce12dec: Added new `@backstage/module-federation-common` package that provides shared types, default configurations, and runtime utilities for module federation. It includes `loadModuleFederationHostShared` for loading shared dependencies in parallel at runtime, `defaultHostSharedDependencies` and `defaultRemoteSharedDependencies` for consistent dependency configuration, and types such as `HostSharedDependencies`, `RemoteSharedDependencies`, and `RuntimeSharedDependenciesGlobal`. diff --git a/packages/module-federation-common/package.json b/packages/module-federation-common/package.json index dfed332525..1670773e3e 100644 --- a/packages/module-federation-common/package.json +++ b/packages/module-federation-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/module-federation-common", - "version": "0.0.0", + "version": "0.1.2-next.0", "description": "Helper library for module federation", "backstage": { "role": "common-library" diff --git a/packages/module-federation-common/src/defaults.ts b/packages/module-federation-common/src/defaults.ts index 27e0e292cf..644d4ee480 100644 --- a/packages/module-federation-common/src/defaults.ts +++ b/packages/module-federation-common/src/defaults.ts @@ -55,7 +55,7 @@ const defaultSharedDependencies = { // MUI v5 // not setting import: false for MUI packages as this // will break once Backstage moves to BUI - '@mui/material/styles/': { + '@mui/material/styles': { host: {}, remote: {}, }, diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 9ef7b91685..43edcf67b5 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/repo-tools +## 0.17.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/cli-common@0.2.0-next.2 + +## 0.17.0-next.1 + +### Minor Changes + +- 0fbcf23: Added support for OpenAPI 3.1 to all `schema openapi` commands. The commands now auto-detect the OpenAPI version from the spec file and use the appropriate generator, supporting both OpenAPI 3.0.x and 3.1.x specifications. + +### Patch Changes + +- 426edbe: Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. +- d5779e5: Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + +## 0.16.6-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 2a51546: Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 18a946c: Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig` +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## 0.16.4 + +### Patch Changes + +- cd75ed0: Add newline to OpenAPI license template files. +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Bump to tar v7 +- 6523040: Support Prettier v3 for api-reports +- be7ebad: Updated package-docs exclude list to reflect renamed example app packages. +- df59ee6: The `type-deps` command now follows relative imports and re-exports into declaration chunk files, and detects ambient global types such as the `jest` namespace. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/cli-common@0.1.18 + - @backstage/cli-node@0.2.18 + ## 0.16.4-next.2 ### Patch Changes diff --git a/packages/repo-tools/openapitools.json b/packages/repo-tools/openapitools.json index 65c0d8ead7..c7593e7a28 100644 --- a/packages/repo-tools/openapitools.json +++ b/packages/repo-tools/openapitools.json @@ -2,6 +2,6 @@ "$schema": "../../node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "6.5.0" + "version": "7.18.0" } } diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 3180dae9a6..c2156beb3e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.16.4-next.2", + "version": "0.17.0-next.2", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" @@ -52,7 +52,7 @@ "@electric-sql/pglite": "^0.3.0", "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.28.1", - "@microsoft/api-extractor": "^7.55.1", + "@microsoft/api-extractor": "^7.57.3", "@openapitools/openapi-generator-cli": "^2.7.0", "@prettier/sync": "^0.6.1", "@stoplight/spectral-core": "^1.18.0", @@ -67,7 +67,7 @@ "chokidar": "^3.5.3", "codeowners-utils": "^1.0.2", "command-exists": "^1.2.9", - "commander": "^12.0.0", + "commander": "^14.0.3", "fs-extra": "^11.2.0", "glob": "^8.0.3", "globby": "^11.0.0", @@ -78,7 +78,7 @@ "knex-pglite": "^0.11.0", "knip": "^5.42.0", "lodash": "^4.17.21", - "minimatch": "^9.0.0", + "minimatch": "^10.2.1", "p-limit": "^3.0.2", "portfinder": "^1.0.32", "tar": "^7.5.6", diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/createTemporaryTsConfig.ts b/packages/repo-tools/src/commands/api-reports/api-reports/createTemporaryTsConfig.ts index 028466dbab..5cf62f8bac 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/createTemporaryTsConfig.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/createTemporaryTsConfig.ts @@ -16,10 +16,10 @@ import fs from 'fs-extra'; import { join } from 'node:path'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; export async function createTemporaryTsConfig(includedPackageDirs: string[]) { - const path = cliPaths.resolveTargetRoot('tsconfig.tmp.json'); + const path = targetPaths.resolveRoot('tsconfig.tmp.json'); process.once('exit', () => { fs.removeSync(path); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts b/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts index a51c9ed17b..1165c24a77 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/generateTypeDeclarations.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { run, ExitCodeError } from '@backstage/cli-common'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; /** * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. @@ -29,7 +29,7 @@ import { paths as cliPaths } from '../../../lib/paths'; * @returns {Promise} A promise that resolves when the declaration files have been generated. */ export async function generateTypeDeclarations(tsconfigFilePath: string) { - await fs.remove(cliPaths.resolveTargetRoot('dist-types')); + await fs.remove(targetPaths.resolveRoot('dist-types')); try { await run( [ @@ -43,7 +43,7 @@ export async function generateTypeDeclarations(tsconfigFilePath: string) { 'false', ], { - cwd: cliPaths.targetRoot, + cwd: targetPaths.rootDir, }, ).waitForExit(); } catch (error) { diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/patchApiReportGeneration.ts b/packages/repo-tools/src/commands/api-reports/api-reports/patchApiReportGeneration.ts index 40778594ec..3ac3eef99b 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/patchApiReportGeneration.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/patchApiReportGeneration.ts @@ -18,7 +18,7 @@ import { ExtractorMessage } from '@microsoft/api-extractor'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; import { Program } from 'typescript'; import { tryRunPrettier } from '../common'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; let applied = false; @@ -162,7 +162,7 @@ export function patchApiReportGeneration() { parser: 'markdown', // We need a real-looking filepath for proper config resolution, not just a directory // Ideally, the real filepath would be better, but it would require too much patching, for very little gain. - filepath: `${cliPaths.targetRoot}/report.api.md`, + filepath: `${targetPaths.rootDir}/report.api.md`, }); }; } diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.test.ts b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.test.ts new file mode 100644 index 0000000000..c99bdb90eb --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2026 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 { TSDocTagSyntaxKind } from '@microsoft/tsdoc'; +import { getTsDocConfig } from './runApiExtraction'; + +describe('getTsDocConfig', () => { + it('should load the base TSDoc config from api-extractor', async () => { + const config = await getTsDocConfig(); + + expect(config).toBeDefined(); + expect(config.filePath).toContain('tsdoc-base.json'); + }); + + it('should add @ignore tag definition with ModifierTag syntax', async () => { + const config = await getTsDocConfig(); + + const ignoreTag = config.tagDefinitions.find( + tag => tag.tagName === '@ignore', + ); + expect(ignoreTag).toBeDefined(); + expect(ignoreTag?.tagName).toBe('@ignore'); + expect(ignoreTag?.syntaxKind).toBe(TSDocTagSyntaxKind.ModifierTag); + }); + + it('should add @config tag definition with BlockTag syntax', async () => { + const config = await getTsDocConfig(); + + const configTag = config.tagDefinitions.find( + tag => tag.tagName === '@config', + ); + expect(configTag).toBeDefined(); + expect(configTag?.tagName).toBe('@config'); + expect(configTag?.syntaxKind).toBe(TSDocTagSyntaxKind.BlockTag); + }); + + it('should enable support for @ignore tag', async () => { + const config = await getTsDocConfig(); + + expect(config.supportForTags.get('@ignore')).toBe(true); + }); + + it('should enable support for @config tag', async () => { + const config = await getTsDocConfig(); + + expect(config.supportForTags.get('@config')).toBe(true); + }); +}); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts index da8278f9ad..6091237fae 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports/runApiExtraction.ts @@ -31,13 +31,11 @@ import { resolve as resolvePath, } from 'node:path'; import { getPackageExportDetails } from '../../../lib/getPackageExportDetails'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import { logApiReportInstructions } from '../common'; import { patchApiReportGeneration } from './patchApiReportGeneration'; -const tmpDir = cliPaths.resolveTargetRoot( - './node_modules/.cache/api-extractor', -); +const tmpDir = targetPaths.resolveRoot('./node_modules/.cache/api-extractor'); export async function countApiReportWarnings(reportPath: string) { try { @@ -100,7 +98,7 @@ async function findPackageEntryPoints(packageDirs: string[]): Promise< return Promise.all( packageDirs.map(async packageDir => { const pkg = await fs.readJson( - cliPaths.resolveTargetRoot(packageDir, 'package.json'), + targetPaths.resolveRoot(packageDir, 'package.json'), ); return getPackageExportDetails(pkg).map(details => { @@ -143,11 +141,7 @@ export async function runApiExtraction({ // inspected. const allDistTypesEntryPointPaths = allEntryPoints.map( ({ packageDir, distTypesPath }) => { - return cliPaths.resolveTargetRoot( - './dist-types', - packageDir, - distTypesPath, - ); + return targetPaths.resolveRoot('./dist-types', packageDir, distTypesPath); }, ); @@ -172,17 +166,15 @@ export async function runApiExtraction({ ? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw)) : allowWarnings; - const projectFolder = cliPaths.resolveTargetRoot(packageDir); - const packageFolder = cliPaths.resolveTargetRoot( - './dist-types', - packageDir, - ); + const projectFolder = targetPaths.resolveRoot(packageDir); + const packageFolder = targetPaths.resolveRoot('./dist-types', packageDir); const remainingReportFiles = new Set( fs.readdirSync(projectFolder).filter( filename => // https://regex101.com/r/QDZIV0/2 filename !== 'knip-report.md' && + !filename.startsWith('cli-report') && !filename.endsWith('.sql.md') && // this has to temporarily match all old api report formats filename.match(/^.*?(api-)?report(-[^.-]+)?(.*?)\.md$/), diff --git a/packages/repo-tools/src/commands/api-reports/buildApiReports.test.ts b/packages/repo-tools/src/commands/api-reports/buildApiReports.test.ts index ab76aec1c1..8346296f44 100644 --- a/packages/repo-tools/src/commands/api-reports/buildApiReports.test.ts +++ b/packages/repo-tools/src/commands/api-reports/buildApiReports.test.ts @@ -16,7 +16,7 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { normalize } from 'node:path'; -import * as pathsLib from '../../lib/paths'; +import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import { categorizePackageDirs } from './categorizePackageDirs'; @@ -51,14 +51,9 @@ jest.mock('./categorizePackageDirs', () => ({ }), })); -const projectPaths = pathsLib.paths; - const mockDir = createMockDirectory(); +overrideTargetPaths(mockDir.path); -jest.spyOn(projectPaths, 'targetRoot', 'get').mockReturnValue(mockDir.path); -jest - .spyOn(projectPaths, 'resolveTargetRoot') - .mockImplementation((...path) => mockDir.resolve(...path)); jest.spyOn(PackageGraph, 'listTargetPackages').mockResolvedValue([ { dir: normalize(mockDir.resolve('packages/package-a')), @@ -85,30 +80,28 @@ jest.spyOn(PackageGraph, 'listTargetPackages').mockResolvedValue([ describe('buildApiReports', () => { beforeEach(() => { mockDir.setContent({ - [projectPaths.targetRoot]: { - 'package.json': JSON.stringify({ - workspaces: { packages: ['packages/*', 'plugins/*'] }, - }), - packages: { - 'package-a': { - 'package.json': '{}', - }, - 'package-b': { - 'package.json': '{}', - }, - 'package-c': {}, - 'README.md': 'Hello World', + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), + packages: { + 'package-a': { + 'package.json': '{}', }, - plugins: { - 'plugin-a': { - 'package.json': '{}', - }, - 'plugin-b': { - 'package.json': '{}', - }, - 'plugin-c': { - 'package.json': '{}', - }, + 'package-b': { + 'package.json': '{}', + }, + 'package-c': {}, + 'README.md': 'Hello World', + }, + plugins: { + 'plugin-a': { + 'package.json': '{}', + }, + 'plugin-b': { + 'package.json': '{}', + }, + 'plugin-c': { + 'package.json': '{}', }, }, }); diff --git a/packages/repo-tools/src/commands/api-reports/buildApiReports.ts b/packages/repo-tools/src/commands/api-reports/buildApiReports.ts index 74b110236f..41c6aa8a57 100644 --- a/packages/repo-tools/src/commands/api-reports/buildApiReports.ts +++ b/packages/repo-tools/src/commands/api-reports/buildApiReports.ts @@ -16,7 +16,8 @@ import { OptionValues } from 'commander'; import { categorizePackageDirs } from './categorizePackageDirs'; -import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import { resolvePackagePaths } from '../../lib/paths'; import { runSqlExtraction } from './sql-reports'; import { runCliExtraction } from './cli-reports'; import { @@ -37,9 +38,7 @@ type Options = { } & OptionValues; export async function buildApiReports(paths: string[] = [], opts: Options) { - const tmpDir = cliPaths.resolveTargetRoot( - './node_modules/.cache/api-extractor', - ); + const tmpDir = targetPaths.resolveRoot('./node_modules/.cache/api-extractor'); const isCiBuild = opts.ci; const isDocsBuild = opts.docs; @@ -73,7 +72,7 @@ export async function buildApiReports(paths: string[] = [], opts: Options) { temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); } const tsconfigFilePath = - temporaryTsConfigPath ?? cliPaths.resolveTargetRoot('tsconfig.json'); + temporaryTsConfigPath ?? targetPaths.resolveRoot('tsconfig.json'); if (runTsc) { console.log('# Compiling TypeScript'); @@ -116,7 +115,7 @@ export async function buildApiReports(paths: string[] = [], opts: Options) { console.log('# Generating package documentation'); await buildDocs({ inputDir: tmpDir, - outputDir: cliPaths.resolveTargetRoot('docs/reference'), + outputDir: targetPaths.resolveRoot('docs/reference'), }); } } diff --git a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts index 86694c3d68..6fdb890100 100644 --- a/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts +++ b/packages/repo-tools/src/commands/api-reports/categorizePackageDirs.ts @@ -15,7 +15,7 @@ */ import fs from 'fs-extra'; -import { paths as cliPaths } from '../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; export async function categorizePackageDirs(packageDirs: string[]) { const dirs = packageDirs.slice(); @@ -34,7 +34,7 @@ export async function categorizePackageDirs(packageDirs: string[]) { } const pkgJson = await fs - .readJson(cliPaths.resolveTargetRoot(dir, 'package.json')) + .readJson(targetPaths.resolveRoot(dir, 'package.json')) .catch(error => { if (error.code === 'ENOENT') { return undefined; @@ -45,9 +45,7 @@ export async function categorizePackageDirs(packageDirs: string[]) { if (!role) { return; // Ignore packages without roles } - if ( - await fs.pathExists(cliPaths.resolveTargetRoot(dir, 'migrations')) - ) { + if (await fs.pathExists(targetPaths.resolveRoot(dir, 'migrations'))) { sqlPackageDirs.push(dir); } // TODO(Rugvip): Inlined packages are ignored because we can't handle @internal exports @@ -56,9 +54,10 @@ export async function categorizePackageDirs(packageDirs: string[]) { if (pkgJson?.backstage?.inline) { return; } - if (role === 'cli') { + if (role === 'cli' || role === 'cli-module') { cliPackageDirs.push(dir); - } else if (role !== 'frontend' && role !== 'backend') { + } + if (role !== 'cli' && role !== 'frontend' && role !== 'backend') { tsPackageDirs.push(dir); } } diff --git a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts index 8aa0eea02c..ce207489a7 100644 --- a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts @@ -22,12 +22,19 @@ import { import fs from 'fs-extra'; import { createBinRunner } from '../../util'; import { CliHelpPage, CliModel } from './types'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import { generateCliReport } from './generateCliReport'; import { logApiReportInstructions } from '../common'; function parseHelpPage(helpPageContent: string) { - const [, usage] = helpPageContent.match(/^\s*Usage: (.*)$/im) ?? []; + let usage: string | undefined; + + // Commander format: "Usage: backstage-cli ..." + const commanderUsage = helpPageContent.match(/^\s*Usage: (.*)$/im); + if (commanderUsage) { + usage = commanderUsage[1]; + } + const lines = helpPageContent.split(/\r?\n/); let options = new Array(); @@ -39,8 +46,8 @@ function parseHelpPage(helpPageContent: string) { lines.shift(); } if (lines.length > 0) { - // Start of a new section, e.g. "Options:" - const sectionName = lines.shift(); + // Start of a new section, e.g. "Options:" or "FLAGS:" + const sectionName = lines.shift()?.toLocaleLowerCase('en-US'); // Take lines until we hit the next section or the end const sectionEndIndex = lines.findIndex( line => line && !line.match(/^\s/), @@ -53,12 +60,18 @@ function parseHelpPage(helpPageContent: string) { .map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1]) .filter(Boolean) as string[]; - if (sectionName?.toLocaleLowerCase('en-US') === 'options:') { + if (sectionName === 'options:' || sectionName === 'flags:') { options = sectionItems; - } else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') { + } else if (sectionName === 'commands:') { commands = sectionItems; - } else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') { + } else if (sectionName === 'arguments:') { commandArguments = sectionItems; + } else if (sectionName === 'usage:') { + // cleye format: usage line is inside the USAGE: section + const usageLine = sectionLines.find(l => l.trim().length > 0)?.trim(); + if (usageLine) { + usage = usageLine; + } } else { throw new Error(`Unknown CLI section: ${sectionName}`); } @@ -115,11 +128,16 @@ export async function runCliExtraction({ }: CliExtractionOptions) { for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); - const fullDir = cliPaths.resolveTargetRoot(packageDir); + const fullDir = targetPaths.resolveRoot(packageDir); const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json')); if (!pkgJson.bin) { - throw new Error(`CLI Package in ${packageDir} has no bin field`); + if (pkgJson.backstage?.role === 'cli') { + throw new Error( + `CLI package ${pkgJson.name} is missing a "bin" field in its package.json`, + ); + } + continue; } const models = new Array(); @@ -162,7 +180,7 @@ export async function runCliExtraction({ console.log(''); console.log( `The conflicting file is ${relativePath( - cliPaths.targetRoot, + targetPaths.rootDir, reportPath, )}, expecting the following content:`, ); diff --git a/packages/repo-tools/src/commands/api-reports/common/tryRunPrettier.ts b/packages/repo-tools/src/commands/api-reports/common/tryRunPrettier.ts index 174b68f499..2c95e71647 100644 --- a/packages/repo-tools/src/commands/api-reports/common/tryRunPrettier.ts +++ b/packages/repo-tools/src/commands/api-reports/common/tryRunPrettier.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import type { Config } from 'prettier'; /** @@ -33,7 +33,7 @@ export async function tryRunPrettierAsync( // Filepath for proper config resolution const filepath = extraConfig.filepath ?? - `${cliPaths.targetRoot}/should-not-be-ignored.any`; + `${targetPaths.rootDir}/should-not-be-ignored.any`; const config = (await prettier.resolveConfig(filepath, { editorconfig: true })) ?? {}; const formattedContent = prettier.format(content, { @@ -68,7 +68,7 @@ export function createPrettierSyncFormatter( // We need a filepath for proper config resolution, not just a directory const filepath = extraConfig.filepath ?? - `${cliPaths.targetRoot}/should-not-be-ignored.any`; + `${targetPaths.rootDir}/should-not-be-ignored.any`; const resolveConfig = // @ts-expect-error: v2 requires .sync, @prettier/sync v3 does not prettierSync.resolveConfig?.sync ?? prettierSync.resolveConfig; diff --git a/packages/repo-tools/src/commands/api-reports/sql-reports/runSqlExtraction.ts b/packages/repo-tools/src/commands/api-reports/sql-reports/runSqlExtraction.ts index 6268d0d30d..375ce07476 100644 --- a/packages/repo-tools/src/commands/api-reports/sql-reports/runSqlExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/sql-reports/runSqlExtraction.ts @@ -16,7 +16,7 @@ import fs, { readJson } from 'fs-extra'; import { relative as relativePath } from 'node:path'; -import { paths as cliPaths } from '../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import { diff as justDiff } from 'just-diff'; import { SchemaInfo } from './types'; import { getPgSchemaInfo } from './getPgSchemaInfo'; @@ -43,14 +43,14 @@ export async function runSqlExtraction(options: SqlExtractionOptions) { let dbIndex = 1; for (const packageDir of options.packageDirs) { - const migrationDir = cliPaths.resolveTargetRoot(packageDir, 'migrations'); + const migrationDir = targetPaths.resolveRoot(packageDir, 'migrations'); if (!(await fs.pathExists(migrationDir))) { console.log(`No SQL migrations found in ${packageDir}`); continue; } const { name: pkgName } = await readJson( - cliPaths.resolveTargetRoot(packageDir, 'package.json'), + targetPaths.resolveRoot(packageDir, 'package.json'), ); const migrationFiles = await fs.readdir(migrationDir, { @@ -95,7 +95,7 @@ async function runSingleSqlExtraction( knex: Knex, options: SqlExtractionOptions, ) { - const migrationDir = cliPaths.resolveTargetRoot( + const migrationDir = targetPaths.resolveRoot( targetDir, 'migrations', migrationTarget, @@ -152,7 +152,7 @@ async function runSingleSqlExtraction( break; } } - const reportPath = cliPaths.resolveTargetRoot( + const reportPath = targetPaths.resolveRoot( targetDir, `report${migrationTarget === '.' ? '' : `-${migrationTarget}`}.sql.md`, ); @@ -182,7 +182,7 @@ async function runSingleSqlExtraction( console.log(''); console.log( `The conflicting file is ${relativePath( - cliPaths.targetRoot, + targetPaths.rootDir, reportPath, )}, expecting the following content:`, ); diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index dab7d6df0d..371d0609ff 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -226,6 +226,7 @@ export function registerCommands(program: Command) { 'CI run checks that there are no changes to catalog-info.yaml files', ) .description('Create or fix info yaml files for all backstage packages') + .allowExcessArguments(true) .action( lazy( () => import('./generate-catalog-info/generate-catalog-info'), diff --git a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts index ab5af976bf..d7eadb3523 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { paths as cliPaths } from '../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import pLimit from 'p-limit'; import os from 'node:os'; import { relative as relativePath, resolve as resolvePath } from 'node:path'; @@ -100,9 +100,9 @@ async function handlePackage({ }: KnipPackageOptions) { console.log(`## Processing ${packageDir}`); - const fullDir = cliPaths.resolveTargetRoot(packageDir); + const fullDir = targetPaths.resolveRoot(packageDir); const reportPath = resolvePath(fullDir, 'knip-report.md'); - const run = createBinRunner(cliPaths.targetRoot, ''); + const run = createBinRunner(targetPaths.rootDir, ''); let report = await run( `${knipDir}/knip.js`, @@ -149,7 +149,7 @@ async function handlePackage({ console.log(''); console.log( `The conflicting file is ${relativePath( - cliPaths.targetRoot, + targetPaths.rootDir, reportPath, )}, expecting the following content:`, ); @@ -168,8 +168,8 @@ export async function runKnipReports({ packageDirs, isLocalBuild, }: KnipExtractionOptions) { - const knipDir = cliPaths.resolveTargetRoot('./node_modules/knip/bin/'); - const knipConfigPath = cliPaths.resolveTargetRoot('./knip.json'); + const knipDir = targetPaths.resolveRoot('./node_modules/knip/bin/'); + const knipConfigPath = targetPaths.resolveRoot('./knip.json'); const limiter = pLimit(os.cpus().length); await generateKnipConfig({ knipConfigPath }); diff --git a/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts b/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts index 3f0424c547..9e2c664e60 100644 --- a/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts +++ b/packages/repo-tools/src/commands/lint-legacy-backend-exports/lint-legacy-backend-exports.ts @@ -16,11 +16,11 @@ import { Project } from 'ts-morph'; import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; import fs from 'fs-extra'; -import { paths as cliPaths } from '../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import path from 'node:path'; const project = new Project({ - tsConfigFilePath: cliPaths.resolveTargetRoot('tsconfig.json'), + tsConfigFilePath: targetPaths.resolveRoot('tsconfig.json'), }); function readPackageJson(pkg: string) { diff --git a/packages/repo-tools/src/commands/package-docs/Cache.test.ts b/packages/repo-tools/src/commands/package-docs/Cache.test.ts index d0293c09b0..02c1f5f427 100644 --- a/packages/repo-tools/src/commands/package-docs/Cache.test.ts +++ b/packages/repo-tools/src/commands/package-docs/Cache.test.ts @@ -24,11 +24,13 @@ import { readFile } from 'node:fs/promises'; import { join as joinPath } from 'node:path'; jest.mock('crypto', () => { + const actual = jest.requireActual('crypto'); const hash = { update: jest.fn(), digest: jest.fn().mockReturnValue('test'), }; return { + ...actual, createHash: jest.fn().mockReturnValue(hash), }; }); diff --git a/packages/repo-tools/src/commands/package-docs/command.ts b/packages/repo-tools/src/commands/package-docs/command.ts index 39568e1b00..3e635b34f4 100644 --- a/packages/repo-tools/src/commands/package-docs/command.ts +++ b/packages/repo-tools/src/commands/package-docs/command.ts @@ -15,7 +15,8 @@ */ import { exec } from 'node:child_process'; import { promisify } from 'node:util'; -import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; +import { resolvePackagePaths } from '../../lib/paths'; import { createTemporaryTsConfig } from './utils'; import { readFile, rm, writeFile } from 'node:fs/promises'; import pLimit from 'p-limit'; @@ -74,7 +75,7 @@ async function generateDocJson(pkg: string) { const temporaryTsConfigPath: string = await createTemporaryTsConfig(pkg); const packageJson = JSON.parse( - await readFile(cliPaths.resolveTargetRoot(pkg, 'package.json'), 'utf-8'), + await readFile(targetPaths.resolveRoot(pkg, 'package.json'), 'utf-8'), ); const exports = getExports(packageJson); @@ -85,17 +86,17 @@ async function generateDocJson(pkg: string) { return false; } - await mkdirp(cliPaths.resolveTargetRoot(`dist-types`, pkg)); + await mkdirp(targetPaths.resolveRoot(`dist-types`, pkg)); const { stdout, stderr } = await execAsync( [ - cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'), + targetPaths.resolveRoot('node_modules/.bin/typedoc'), '--json', - cliPaths.resolveTargetRoot(`dist-types`, pkg, 'docs.json'), + targetPaths.resolveRoot(`dist-types`, pkg, 'docs.json'), '--tsconfig', temporaryTsConfigPath, '--basePath', - cliPaths.targetRoot, + targetPaths.rootDir, '--skipErrorChecking', ...(getExports(packageJson).flatMap(e => [ '--entryPoints', @@ -117,7 +118,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { console.warn('!!! This is an experimental command !!!'); const existingDocsJsonPaths = glob.sync( - cliPaths.resolveTargetRoot('dist-types/**/docs.json'), + targetPaths.resolveRoot('dist-types/**/docs.json'), ); if (existingDocsJsonPaths.length > 0) { console.warn( @@ -129,7 +130,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { } } console.warn('!!! Deleting existing docs output !!!'); - await rm(cliPaths.resolveTargetRoot('type-docs'), { + await rm(targetPaths.resolveRoot('type-docs'), { recursive: true, force: true, }); @@ -140,8 +141,8 @@ export default async function packageDocs(paths: string[] = [], opts: any) { }); const cache = await PackageDocsCache.loadAsync( - cliPaths.resolveTargetRoot(), - await Lockfile.load(cliPaths.resolveTargetRoot('yarn.lock')), + targetPaths.rootDir, + await Lockfile.load(targetPaths.resolveRoot('yarn.lock')), ); console.log(`### Generating docs.`); @@ -149,10 +150,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { selectedPackageDirs.map(pkg => limit(async () => { const pkgJson = JSON.parse( - await readFile( - cliPaths.resolveTargetRoot(pkg, 'package.json'), - 'utf-8', - ), + await readFile(targetPaths.resolveRoot(pkg, 'package.json'), 'utf-8'), ); if (EXCLUDE.includes(pkg) || pkgJson.name.startsWith('@internal/')) { return; @@ -171,10 +169,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { console.log(`### Processing ${pkg}`); const success = await generateDocJson(pkg); if (success) { - await cache.write( - pkg, - cliPaths.resolveTargetRoot(`dist-types`, pkg), - ); + await cache.write(pkg, targetPaths.resolveRoot(`dist-types`, pkg)); } } catch (e) { console.error('Failed to generate docs for', pkg); @@ -187,7 +182,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { const generatedPackageDirs = []; for (const pkg of selectedPackageDirs) { try { - const docsJsonPath = cliPaths.resolveTargetRoot( + const docsJsonPath = targetPaths.resolveRoot( `dist-types/${pkg}/docs.json`, ); const docsJson = JSON.parse(await readFile(docsJsonPath, 'utf-8')); @@ -211,7 +206,7 @@ export default async function packageDocs(paths: string[] = [], opts: any) { const { stdout, stderr } = await execAsync( [ - cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'), + targetPaths.resolveRoot('node_modules/.bin/typedoc'), '--entryPointStrategy', 'merge', ...generatedPackageDirs.flatMap(pkg => [ @@ -220,13 +215,13 @@ export default async function packageDocs(paths: string[] = [], opts: any) { ]), ...HIGHLIGHT_LANGUAGES.flatMap(e => ['--highlightLanguages', e]), '--out', - cliPaths.resolveTargetRoot('type-docs'), - ...(existsSync(cliPaths.resolveTargetRoot('typedoc.json')) - ? ['--options', cliPaths.resolveTargetRoot('typedoc.json')] + targetPaths.resolveRoot('type-docs'), + ...(existsSync(targetPaths.resolveRoot('typedoc.json')) + ? ['--options', targetPaths.resolveRoot('typedoc.json')] : []), ].join(' '), { - cwd: cliPaths.targetRoot, + cwd: targetPaths.rootDir, }, ); diff --git a/packages/repo-tools/src/commands/package-docs/utils.ts b/packages/repo-tools/src/commands/package-docs/utils.ts index 2cb95443b6..21294daa79 100644 --- a/packages/repo-tools/src/commands/package-docs/utils.ts +++ b/packages/repo-tools/src/commands/package-docs/utils.ts @@ -14,11 +14,15 @@ * limitations under the License. */ +import { findOwnPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; -import { paths as cliPaths } from '../../lib/paths'; export async function createTemporaryTsConfig(dir: string) { - const path = cliPaths.resolveOwnRoot(dir, 'tsconfig.typedoc.tmp.json'); + /* eslint-disable-next-line no-restricted-syntax */ + const path = findOwnPaths(__dirname).resolveRoot( + dir, + 'tsconfig.typedoc.tmp.json', + ); process.once('exit', () => { fs.removeSync(path); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/diff.ts b/packages/repo-tools/src/commands/package/schema/openapi/diff.ts index 2f0af30881..851b2b3a48 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/diff.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/diff.ts @@ -16,7 +16,7 @@ import chalk from 'chalk'; import { exec } from '../../../../lib/exec'; import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers'; -import { paths as cliPaths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import { OptionValues } from 'commander'; import { env } from 'node:process'; import { readFile, rm } from 'node:fs/promises'; @@ -53,7 +53,7 @@ async function check(opts: OptionValues) { baseRef, ], { - cwd: cliPaths.targetRoot, + cwd: targetPaths.rootDir, env: { CI: opts.json ? '1' : undefined, ...env }, }, ); @@ -65,7 +65,7 @@ async function check(opts: OptionValues) { if (opts.json) { const file = ( - await readFile(resolve(cliPaths.targetRoot, 'ci-run-details.json')) + await readFile(resolve(targetPaths.rootDir, 'ci-run-details.json')) ).toString(); const results = JSON.parse(file); console.log(file); @@ -73,7 +73,7 @@ async function check(opts: OptionValues) { throw new Error('Some checks failed'); } - await rm(resolve(cliPaths.targetRoot, 'ci-run-details.json')); + await rm(resolve(targetPaths.rootDir, 'ci-run-details.json')); } else { console.log(reduceOpticOutput(output)); if (!opts.ignore && failed) { diff --git a/packages/repo-tools/src/commands/package/schema/openapi/fuzz.ts b/packages/repo-tools/src/commands/package/schema/openapi/fuzz.ts index 29b038ac5a..12e5d7d527 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/fuzz.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/fuzz.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import fs from 'fs-extra'; -import { paths as cliPaths } from '../../../../lib/paths'; +import { targetPaths } from '@backstage/cli-common'; import chalk from 'chalk'; import { spawn } from '../../../../lib/exec'; import { getPathToCurrentOpenApiSpec } from '../../../../lib/openapi/helpers'; @@ -40,7 +40,7 @@ async function fuzz(opts: OptionValues) { await fs.readFile(resolvedOpenapiPath, 'utf8'), ) as { info: { title: string } }; const configSource = ConfigSources.default({ - rootDir: cliPaths.targetRoot, + rootDir: targetPaths.rootDir, }); const config = await ConfigSources.toConfig(configSource); const pluginId = openapiSpec.info.title; @@ -48,7 +48,7 @@ async function fuzz(opts: OptionValues) { if (opts.debug) { args.push( '--cassette-path', - cliPaths.resolveTargetRoot(join('.cassettes', `${pluginId}.yml`)), + targetPaths.resolveRoot(join('.cassettes', `${pluginId}.yml`)), ); } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 24f8496a0d..42e9a9e682 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -24,11 +24,12 @@ import { OUTPUT_PATH, } from '../../../../../lib/openapi/constants'; import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; +import { targetPaths } from '@backstage/cli-common'; import { + getOpenApiGeneratorKey, getPathToCurrentOpenApiSpec, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; -import { paths as cliPaths } from '../../../../../lib/paths'; async function generate( outputDirectory: string, @@ -36,13 +37,14 @@ async function generate( abortSignal?: AbortController, ) { const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); - const resolvedOutputDirectory = cliPaths.resolveTargetRoot( + const resolvedOutputDirectory = targetPaths.resolveRoot( outputDirectory, OUTPUT_PATH, ); const additionalProperties = toGeneratorAdditionalProperties({ initialValue: clientAdditionalProperties, }); + const generatorKey = await getOpenApiGeneratorKey(resolvedOpenapiPath); await fs.emptyDir(resolvedOutputDirectory); @@ -68,7 +70,7 @@ async function generate( 'templates/typescript-backstage-client.yaml', ), '--generator-key', - 'v3.0', + generatorKey, additionalProperties ? `--additional-properties=${additionalProperties}` : '', @@ -87,7 +89,7 @@ async function generate( await fs.writeFile( resolve(parentDirectory, 'index.ts'), - `// + `// export * from './generated';`, ); @@ -95,8 +97,8 @@ async function generate( signal: abortSignal?.signal, }); - const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier'); - if (prettier) { + const prettier = targetPaths.resolveRoot('node_modules/.bin/prettier'); + if (await fs.pathExists(prettier)) { await exec(`${prettier} --write ${parentDirectory}`, [], { signal: abortSignal?.signal, }); @@ -111,7 +113,12 @@ async function generate( } fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); + fs.removeSync(resolve(resolvedOutputDirectory, '.gitattributes')); + fs.rmSync(resolve(resolvedOutputDirectory, 'docs'), { + recursive: true, + force: true, + }); fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), { recursive: true, force: true, diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index be70ee68c4..daaf619450 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -27,23 +27,24 @@ import { TS_SCHEMA_PATH, } from '../../../../../lib/openapi/constants'; import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; +import { targetPaths } from '@backstage/cli-common'; import { + getOpenApiGeneratorKey, getPathToCurrentOpenApiSpec, getRelativePathToFile, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; -import { paths as cliPaths } from '../../../../../lib/paths'; async function generateSpecFile() { const openapiPath = await getPathToCurrentOpenApiSpec(); const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); - const tsPath = cliPaths.resolveTarget(TS_SCHEMA_PATH); + const tsPath = targetPaths.resolve(TS_SCHEMA_PATH); const schemaDir = dirname(tsPath); await fs.mkdirp(schemaDir); - const oldTsPath = cliPaths.resolveTarget(OLD_SCHEMA_PATH); + const oldTsPath = targetPaths.resolve(OLD_SCHEMA_PATH); if (fs.existsSync(oldTsPath)) { console.warn(`Removing old schema file at ${oldTsPath}`); fs.removeSync(oldTsPath); @@ -77,9 +78,11 @@ export const createOpenApiRouter = async ( ); await exec(`yarn backstage-cli package lint`, ['--fix', tsPath, indexFile]); - if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) { + if ( + await fs.pathExists(targetPaths.resolveRoot('node_modules/.bin/prettier')) + ) { await exec(`yarn prettier`, ['--write', tsPath, indexFile], { - cwd: cliPaths.targetRoot, + cwd: targetPaths.rootDir, }); } } @@ -101,6 +104,7 @@ async function generate( const additionalProperties = toGeneratorAdditionalProperties({ initialValue: serverAdditionalProperties, }); + const generatorKey = await getOpenApiGeneratorKey(resolvedOpenapiPath); await exec( 'node', @@ -119,7 +123,7 @@ async function generate( 'templates/typescript-backstage-server.yaml', ), '--generator-key', - 'v3.0', + generatorKey, additionalProperties ? `--additional-properties=${additionalProperties}` : '', @@ -150,7 +154,7 @@ async function generate( }, ); - const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier'); + const prettier = targetPaths.resolveRoot('node_modules/.bin/prettier'); if (prettier) { await exec(`${prettier} --write ${resolvedOutputDirectory}`, [], { signal: abortSignal?.signal, @@ -158,7 +162,12 @@ async function generate( } fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); + fs.removeSync(resolve(resolvedOutputDirectory, '.gitattributes')); + fs.rmSync(resolve(resolvedOutputDirectory, 'docs'), { + recursive: true, + force: true, + }); fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), { recursive: true, force: true, diff --git a/packages/repo-tools/src/commands/package/schema/openapi/init.ts b/packages/repo-tools/src/commands/package/schema/openapi/init.ts index 00e82f65df..04b6e73483 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/init.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/init.ts @@ -14,8 +14,8 @@ * limitations under the License. */ import fs from 'fs-extra'; +import { targetPaths } from '@backstage/cli-common'; import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; -import { paths as cliPaths } from '../../../../lib/paths'; import chalk from 'chalk'; import { exec } from '../../../../lib/exec'; import { @@ -49,7 +49,7 @@ capture: ${YAML_SCHEMA_PATH}: # 🔧 Runnable example with simple get requests. # Run with "PORT=3000 optic capture ${YAML_SCHEMA_PATH} --update interactive" in '${ - cliPaths.targetDir + targetPaths.dir }' # You can change the server and the 'requests' section to experiment server: @@ -61,10 +61,12 @@ capture: # 🔧 Specify a command that will generate traffic command: yarn backstage-cli package test --no-watch ${ROUTER_TEST_PATHS.map( e => `"${e}"`, - ).join(' ')} + ).join(' ')} `, ); - if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) { + if ( + await fs.pathExists(targetPaths.resolveRoot('node_modules/.bin/prettier')) + ) { await exec(`yarn prettier`, ['--write', opticConfigFilePath]); } } diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/diff.ts b/packages/repo-tools/src/commands/repo/schema/openapi/diff.ts index 02f3db0c43..2648f78f06 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/diff.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/diff.ts @@ -16,16 +16,16 @@ import { PackageGraph } from '@backstage/cli-node'; import { OptionValues } from 'commander'; import { exec } from '../../../../lib/exec'; +import { targetPaths } from '@backstage/cli-common'; import { CiRunDetails, generateCompareSummaryMarkdown, } from '../../../../lib/openapi/optic/helpers'; -import { paths as cliPaths } from '../../../../lib/paths'; import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; function cleanUpApiName(e: { apiName: string }) { e.apiName = e.apiName - .replace(cliPaths.targetDir, '') + .replace(targetPaths.dir, '') .replace(YAML_SCHEMA_PATH, ''); } @@ -46,7 +46,7 @@ export async function command(opts: OptionValues) { const changedOpenApiSpecs = changedFiles .split('\n') .filter(e => e.endsWith(YAML_SCHEMA_PATH)) - .map(e => cliPaths.resolveTarget(e)); + .map(e => targetPaths.resolve(e)); // filter packages by changedFiles packages = packages.filter(pkg => diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts index 34c8589d54..91f43c4cee 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts @@ -49,13 +49,15 @@ async function lint(directoryPath: string, config?: { strict: boolean }) { { extends: [oas, ruleset], rules: { - 'allow-reserved-in-params': { - given: '$.paths..parameters[*]', + 'allow-reserved-in-query-params': { + given: '$.paths..parameters[?(@.in == "query")]', then: { field: 'allowReserved', function: truthy, }, severity: 'error', + message: + 'Query parameters must specify allowReserved (true or false)', }, }, overrides: [ diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/test.ts b/packages/repo-tools/src/commands/repo/schema/openapi/test.ts index 233c50a1fa..e735e729c6 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/test.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/test.ts @@ -17,9 +17,9 @@ import fs from 'fs-extra'; import { join } from 'node:path'; import chalk from 'chalk'; +import { findOwnPaths, targetPaths } from '@backstage/cli-common'; import { runner } from '../../../../lib/runner'; import { YAML_SCHEMA_PATH } from '../../../../lib/openapi/constants'; -import { paths as cliPaths } from '../../../../lib/paths'; import { exec } from '../../../../lib/exec'; import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; @@ -42,7 +42,10 @@ async function test( let opticLocation = ''; try { opticLocation = ( - await exec(`yarn bin optic`, [], { cwd: cliPaths.ownRoot }) + await exec(`yarn bin optic`, [], { + /* eslint-disable-next-line no-restricted-syntax */ + cwd: findOwnPaths(__dirname).rootDir, + }) ).stdout as string; } catch (err) { throw new Error( @@ -79,7 +82,9 @@ async function test( throw err; } if ( - (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) && + (await fs.pathExists( + targetPaths.resolveRoot('node_modules/.bin/prettier'), + )) && options?.update ) { await exec(`yarn prettier`, ['--write', openapiPath]); diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts index 2aa0392fae..9834009d09 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts @@ -19,8 +19,8 @@ import { isEqual } from 'lodash'; import { join } from 'node:path'; import chalk from 'chalk'; import { relative as relativePath, resolve as resolvePath } from 'node:path'; +import { targetPaths } from '@backstage/cli-common'; import { runner } from '../../../../lib/runner'; -import { paths as cliPaths } from '../../../../lib/paths'; import { OLD_SCHEMA_PATH, TS_SCHEMA_PATH, @@ -60,7 +60,7 @@ async function verify(directoryPath: string) { throw new Error(`\`${TS_SCHEMA_PATH}\` needs to have a 'spec' export.`); } if (!isEqual(schema.spec, yaml)) { - const path = relativePath(cliPaths.targetRoot, directoryPath); + const path = relativePath(targetPaths.rootDir, directoryPath); throw new Error( `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools package schema openapi generate\` from '${path}' to regenerate \`${TS_SCHEMA_PATH}\`.`, ); diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index 3ca8fa2f17..bb29cdf441 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -14,58 +14,63 @@ * limitations under the License. */ -import { spawn } from 'node:child_process'; -import os from 'node:os'; -import pLimit from 'p-limit'; +import { spawnSync } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { openSync, closeSync, readFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -// Some commands launch full node processes doing heavy work, which at high -// concurrency levels risk exhausting system resources. Placing the limiter here -// at the root level ensures that the concurrency boundary applies globally, not -// just per-runner. -const limiter = pLimit(os.cpus().length); +// Matches ANSI SGR escape sequences (e.g. bold, color, reset) +const ansiPattern = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, 'g'); +/** + * Redirect stdout to a temp file so that Node.js creates a SyncWriteStream + * (synchronous writes) in the child instead of an async pipe stream. This + * prevents data loss when child processes call process.exit() before the + * async stream buffer has been flushed. + * + * Uses spawnSync which blocks the event loop, so no concurrency limiter is + * needed — each call naturally runs sequentially. + */ export function createBinRunner(cwd: string, path: string) { - return async (...command: string[]) => - limiter( - () => - new Promise((resolve, reject) => { - // Handle the case where path is empty and the script path is the first command argument - const args = path ? [path, ...command] : command; - const child = spawn('node', args, { - cwd, - stdio: ['ignore', 'pipe', 'pipe'], - }); + return async (...command: string[]) => { + const args = path ? [path, ...command] : command; + const outPath = join(tmpdir(), `backstage-cli-out-${randomUUID()}.txt`); + const outFd = openSync(outPath, 'w'); - let stdout = ''; - let stderr = ''; + try { + const result = spawnSync('node', args, { + cwd, + env: { ...process.env, NO_COLOR: '1' }, + stdio: ['ignore', outFd, 'pipe'], + }); - child.stdout?.on('data', data => { - stdout += data.toString(); - }); + closeSync(outFd); + const stdout = readFileSync(outPath, 'utf8').replace(ansiPattern, ''); - child.stderr?.on('data', data => { - stderr += data.toString(); - }); + if (result.error) { + throw new Error(`Process error: ${result.error.message}`); + } - child.on('error', err => { - reject(new Error(`Process error: ${err.message}`)); - }); + const stderr = result.stderr?.toString() ?? ''; - child.on('close', (code, signal) => { - if (signal) { - reject( - new Error( - `Process was killed with signal ${signal}\n${stderr}`, - ), - ); - } else if (code !== 0) { - reject(new Error(`Process exited with code ${code}\n${stderr}`)); - } else if (stderr.trim()) { - reject(new Error(`Command printed error output: ${stderr}`)); - } else { - resolve(stdout); - } - }); - }), - ); + if (result.signal) { + throw new Error( + `Process was killed with signal ${result.signal}\n${stderr}`, + ); + } else if (result.status !== 0) { + throw new Error(`Process exited with code ${result.status}\n${stderr}`); + } else if (stderr.trim()) { + throw new Error(`Command printed error output: ${stderr}`); + } + + return stdout; + } finally { + try { + unlinkSync(outPath); + } catch { + /* ignore cleanup errors */ + } + } + }; } diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index 5d1bcf58c7..c4cd1564c6 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -18,8 +18,8 @@ import Parser from '@apidevtools/swagger-parser'; import fs, { pathExists } from 'fs-extra'; import YAML from 'js-yaml'; import { cloneDeep } from 'lodash'; +import { targetPaths } from '@backstage/cli-common'; import { resolve } from 'node:path'; -import { paths } from '../paths'; import { YAML_SCHEMA_PATH } from './constants'; export const getPathToFile = async (directory: string, filename: string) => { @@ -27,7 +27,7 @@ export const getPathToFile = async (directory: string, filename: string) => { }; export const getRelativePathToFile = async (filename: string) => { - return await getPathToFile(paths.targetDir, filename); + return await getPathToFile(targetPaths.dir, filename); }; export const assertExists = async (path: string) => { @@ -70,3 +70,32 @@ export function toGeneratorAdditionalProperties({ .map(([key, value]) => `${key}=${value}`) .join(','); } + +export async function getOpenApiGeneratorKey( + specPath: string, +): Promise { + const yaml = (await loadAndValidateOpenApiYaml(specPath)) as any; + const version = yaml.openapi; + + if (!version) { + throw new Error(`Could not determine OpenAPI version from ${specPath}`); + } + + const semver = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(version); + if (!semver) { + throw new Error(`Invalid OpenAPI version format ${version} in ${specPath}`); + } + const [, major, minor] = semver; + const supportedVersions = ['3.0', '3.1']; + + const majorMinor = `${major}.${minor}`; + if (!supportedVersions.includes(majorMinor)) { + throw new Error( + `Unsupported OpenAPI version ${version} in ${specPath}. Supported versions are: ${supportedVersions.join( + ', ', + )}`, + ); + } + + return `v${majorMinor}`; +} diff --git a/packages/repo-tools/src/lib/paths.ts b/packages/repo-tools/src/lib/paths.ts index ccca6f09c9..fea456ea3e 100644 --- a/packages/repo-tools/src/lib/paths.ts +++ b/packages/repo-tools/src/lib/paths.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import { PackageGraph } from '@backstage/cli-node'; import { Minimatch } from 'minimatch'; import { isAbsolute, relative as relativePath } from 'node:path'; -/* eslint-disable-next-line no-restricted-syntax */ -export const paths = findPaths(__dirname); - /** @internal */ export interface ResolvePackagesOptions { paths?: string[]; @@ -41,7 +38,7 @@ export async function resolvePackagePaths( for (const path of providedPaths) { const matches = packages.some( ({ dir }) => - new Minimatch(path).match(relativePath(paths.targetRoot, dir)) || + new Minimatch(path).match(relativePath(targetPaths.rootDir, dir)) || isChildPath(dir, path), ); if (!matches) { @@ -57,7 +54,7 @@ export async function resolvePackagePaths( packages = packages.filter(({ dir }) => providedPaths.some( path => - new Minimatch(path).match(relativePath(paths.targetRoot, dir)) || + new Minimatch(path).match(relativePath(targetPaths.rootDir, dir)) || isChildPath(dir, path), ), ); @@ -66,7 +63,9 @@ export async function resolvePackagePaths( if (include) { packages = packages.filter(pkg => include.some(pattern => - new Minimatch(pattern).match(relativePath(paths.targetRoot, pkg.dir)), + new Minimatch(pattern).match( + relativePath(targetPaths.rootDir, pkg.dir), + ), ), ); } @@ -76,13 +75,13 @@ export async function resolvePackagePaths( exclude.some( pattern => !new Minimatch(pattern).match( - relativePath(paths.targetRoot, pkg.dir), + relativePath(targetPaths.rootDir, pkg.dir), ), ), ); } - return packages.map(pkg => relativePath(paths.targetRoot, pkg.dir)); + return packages.map(pkg => relativePath(targetPaths.rootDir, pkg.dir)); } /** @internal */ diff --git a/packages/repo-tools/src/lib/runner.ts b/packages/repo-tools/src/lib/runner.ts index 7061a195ad..193a267cf0 100644 --- a/packages/repo-tools/src/lib/runner.ts +++ b/packages/repo-tools/src/lib/runner.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import { targetPaths } from '@backstage/cli-common'; import { resolvePackagePaths } from './paths'; import pLimit from 'p-limit'; import { relative as relativePath } from 'node:path'; -import { paths as cliPaths } from './paths'; import portFinder from 'portfinder'; export async function runner( @@ -58,7 +58,7 @@ export async function runner( } return { - relativeDir: relativePath(cliPaths.targetRoot, pkg), + relativeDir: relativePath(targetPaths.rootDir, pkg), resultText, }; }), diff --git a/packages/scaffolder-internal/CHANGELOG.md b/packages/scaffolder-internal/CHANGELOG.md index f35d543722..1737ef3b7e 100644 --- a/packages/scaffolder-internal/CHANGELOG.md +++ b/packages/scaffolder-internal/CHANGELOG.md @@ -1,5 +1,29 @@ # @internal/scaffolder +## 0.0.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + +## 0.0.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-scaffolder-react@1.19.7 + ## 0.0.18-next.0 ### Patch Changes diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 2139b6bfc9..1bbd38b578 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/scaffolder", - "version": "0.0.18-next.0", + "version": "0.0.19-next.1", "backstage": { "role": "web-library", "inline": true diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index c5904a1720..ac9c2f6ac9 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,82 @@ # techdocs-cli-embedded-app +## 0.2.118-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/cli@0.36.0-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + - @backstage/frontend-defaults@0.5.0-next.1 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 0.2.118-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 0.2.118-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 0.2.117 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-defaults@0.4.0 + - @backstage/cli@0.35.4 + - @backstage/core-components@0.18.7 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/test-utils@1.7.15 + ## 0.2.117-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 6dcda2a2fb..b8cef528ce 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.117-next.1", + "version": "0.2.118-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 10ebdbdf99..5eff559128 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,41 @@ # @techdocs/cli +## 1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## 1.10.6-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## 1.10.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 27798df: Migrate the Techdocs CLI embedded app to the New Frontend System (NFS) +- 508d127: Updated dependency `find-process` to `^2.0.0`. +- Updated dependencies + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/cli-common@0.1.18 + ## 1.10.5-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index 4cb7988add..667c4fb6b6 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -39,7 +39,7 @@ Options: --source-dir --techdocs-ref -h, --help - -v --verbose + -v, --verbose ``` ### `techdocs-cli migrate` @@ -62,7 +62,7 @@ Options: --removeOriginal --storage-name -h, --help - -v --verbose + -v, --verbose ``` ### `techdocs-cli publish` @@ -112,7 +112,7 @@ Options: -c, --mkdocs-config-file-name -h, --help -i, --docker-image - -v --verbose + -v, --verbose ``` ### `techdocs-cli serve:mkdocs` @@ -128,5 +128,5 @@ Options: -h, --help -i, --docker-image -p, --port - -v --verbose + -v, --verbose ``` diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index fb33677b45..8b6a70f952 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,6 +1,6 @@ { "name": "@techdocs/cli", - "version": "1.10.5-next.1", + "version": "1.10.6-next.1", "description": "Utility CLI for managing TechDocs sites in Backstage.", "backstage": { "role": "cli" @@ -49,7 +49,7 @@ "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-techdocs-node": "workspace:^", - "commander": "^12.0.0", + "commander": "^14.0.3", "fs-extra": "^11.0.0", "global-agent": "^3.0.0", "http-proxy": "^1.18.1", @@ -59,7 +59,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@types/commander": "^2.12.2", "@types/fs-extra": "^11.0.0", "@types/http-proxy": "^1.17.4", "@types/node": "^22.13.14", diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 253a2900fd..e69cf4671a 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -59,7 +59,7 @@ export function registerCommands(program: Command) { 'Name for site when using default MkDocs config', 'Documentation Site', ) - .option('-v --verbose', 'Enable verbose output.', false) + .option('-v, --verbose', 'Enable verbose output.', false) .option( '--omitTechdocsCoreMkdocsPlugin', "Don't patch MkDocs file automatically with techdocs-core plugin.", @@ -142,7 +142,7 @@ export function registerCommands(program: Command) { 'Optional Controls the number of API requests allowed to be performed simultaneously.', '25', ) - .option('-v --verbose', 'Enable verbose output.', false) + .option('-v, --verbose', 'Enable verbose output.', false) .action(lazy(() => import('./migrate/migrate'), 'default')); program @@ -253,7 +253,7 @@ export function registerCommands(program: Command) { 'Documentation Site', ) .option('-p, --port ', 'Port to serve documentation locally', '8000') - .option('-v --verbose', 'Enable verbose output.', false) + .option('-v, --verbose', 'Enable verbose output.', false) .action(lazy(() => import('./serve/mkdocs'), 'default')); program @@ -284,7 +284,7 @@ export function registerCommands(program: Command) { 'Documentation Site', ) .option('--mkdocs-port ', 'Port for MkDocs server to use', '8000') - .option('-v --verbose', 'Enable verbose output.', false) + .option('-v, --verbose', 'Enable verbose output.', false) .option( '--preview-app-bundle-path ', 'Preview documentation using another web app', diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 864861828b..10b7e1498f 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -17,7 +17,7 @@ import { OptionValues } from 'commander'; import path from 'node:path'; import openBrowser from 'react-dev-utils/openBrowser'; -import { findPaths, RunOnOutput } from '@backstage/cli-common'; +import { findOwnPaths, RunOnOutput } from '@backstage/cli-common'; import HTTPServer from '../../lib/httpServer'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { createLogger } from '../../lib/utility'; @@ -39,8 +39,8 @@ function findPreviewBundlePath(): string { // This can be tested by running `yarn pack` and extracting the resulting tarball into a directory. // Within the extracted directory, run `npm install --only=prod`. // Once that's done you can test the CLI in any directory using `node /package `. - // eslint-disable-next-line no-restricted-syntax - return findPaths(__dirname).resolveOwn('dist/embedded-app'); + /* eslint-disable-next-line no-restricted-syntax */ + return findOwnPaths(__dirname).resolve('dist/embedded-app'); } } diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 06d257696c..86a6b08bb6 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/test-utils +## 1.7.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 1.7.15 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 68eb322: Added `@types/jest` as an optional peer dependency, since jest types are exposed in the public API surface. +- Updated dependencies + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-permission-common@0.9.6 + ## 1.7.15-next.2 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 73ca74570c..c5b4f771d2 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.7.15-next.2", + "version": "1.7.16-next.0", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 405b9ae6fc..560e452e46 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/theme +## 0.7.2 + +### Patch Changes + +- 1c52dcc: add square shape +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + ## 0.7.2-next.1 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 9ddb38d52f..e95ae20b36 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.7.2-next.1", + "version": "0.7.2", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/packages/theme/report.api.md b/packages/theme/report.api.md index f4e31db8e9..064f6f3e6c 100644 --- a/packages/theme/report.api.md +++ b/packages/theme/report.api.md @@ -185,7 +185,7 @@ export function createBaseThemeOptions( palette: PaletteOptions; typography: BackstageTypography; page: PageTheme; - getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; + getPageTheme: (input: PageThemeSelector) => PageTheme; }; // @public @deprecated diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 7b42996707..c2eebd60e9 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,525 @@ # @backstage/ui +## 0.13.0-next.2 + +### Patch Changes + +- db92751: Added interactive support to the `Card` component. Pass `onPress` to make the entire card surface pressable, or `href` to make it navigate to a URL. A transparent overlay handles the interaction while nested buttons and links remain independently clickable. +- 12d8afe: Added analytics capabilities to the component library. Components with navigation behavior (Link, ButtonLink, Tab, MenuItem, Tag, Row) now fire analytics events on click when a `BUIProvider` is present. + + New exports: `BUIProvider`, `useAnalytics`, `getNodeText`, and associated types (`AnalyticsTracker`, `UseAnalyticsFn`, `BUIProviderProps`, `AnalyticsEventAttributes`). + + Components with analytics support now accept a `noTrack` prop to suppress event firing. + + **Affected components:** Link, ButtonLink, Tab, MenuItem, Tag, Row + +- 430d5ed: Fixed interactive cards so that CardBody can scroll when the card has a constrained height. Previously, the overlay element blocked scroll events. + + **Affected components:** Card + +- ad7c883: Deprecated the `HeaderPage` component name in favor of `Header`. The old `HeaderPage`, `HeaderPageProps`, `HeaderPageOwnProps`, `HeaderPageBreadcrumb`, and `HeaderPageDefinition` exports are still available as deprecated aliases. +- f42f4cc: Fixed Table column headers overflowing and wrapping when there is not enough space. Headers now truncate with ellipsis instead. + + **Affected components:** Table + +- fbd5c5a: Fixed Table rows showing a pointer cursor when not interactive. Rows now only show `cursor: pointer` when they have an `href`, are selectable, or are pressable. + + **Affected components:** Table + +- 7960d54: Added support for native HTML div attributes on the `Flex`, `Grid`, and `Grid.Item` components. + + **Affected components:** Flex, Grid, Grid.Item + +- 12d8afe: Fixed MenuItem `onAction` prop ordering so user-provided `onAction` handlers are chained rather than silently overwritten. +- bb66b86: The `Select` trigger now automatically adapts its background colour based on the parent background context. + + **Affected components:** Select + +- 934ac03: `SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token. + + **Affected components:** SearchField, TextField + +## 0.13.0-next.1 + +### Minor Changes + +- 768f09d: **BREAKING**: Simplified the neutral background prop API for container components. The explicit `neutral-1`, `neutral-2`, `neutral-3`, and `neutral-auto` values have been removed from `ProviderBg`. They are replaced by a single `'neutral'` value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. + + **Migration:** + + Replace any explicit `bg="neutral-1"`, `bg="neutral-2"`, `bg="neutral-3"`, or `bg="neutral-auto"` props with `bg="neutral"`. To achieve a specific neutral level in stories or tests, use nested containers — each additional `bg="neutral"` wrapper increments by one level. + + ```tsx + // Before + ... + + // After + + ... + + ``` + + **Affected components:** Box, Flex, Grid, Card, Accordion, Popover, Tooltip, Dialog, Menu + +- b42fcdc: **BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token. + + **Migration:** + + Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells): + + ```diff + - background: var(--bui-bg-popover); + + background: var(--bui-bg-neutral-1); + ``` + + **Affected components:** Popover, Tooltip, Menu, Dialog + +- bd3a76e: **BREAKING**: Data attributes rendered by components are now always lowercase. This affects CSS selectors targeting camelCase data attributes. + + **Migration:** + + Update any custom CSS selectors that target camelCase data attributes to use lowercase instead: + + ```diff + - [data-startCollapsed='true'] { ... } + + [data-startcollapsed='true'] { ... } + ``` + + **Affected components:** SearchField + +- 95702ab: **BREAKING**: Removed deprecated types `ComponentDefinition`, `ClassNamesMap`, `DataAttributeValues`, and `DataAttributesMap` from the public API. These were internal styling infrastructure types that have been replaced by the `defineComponent` system. + + **Migration:** + + Remove any direct usage of these types. Component definitions now use `defineComponent()` and their shapes are not part of the public API contract. + + ```diff + - import type { ComponentDefinition, ClassNamesMap } from '@backstage/ui'; + ``` + + If you were reading `definition.dataAttributes`, use `definition.propDefs` instead — props with `dataAttribute: true` in `propDefs` are the equivalent. + +### Patch Changes + +- 58224d3: Fixed neutral-1 hover & pressed state in light mode. +- 95702ab: Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + + **Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden + +- 4c2c350: Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + + Affected components: Container + +- d4fa5b4: Fixed tab `matchStrategy` matching to ignore query parameters and hash fragments in tab `href` values. Previously, tabs with query params in their `href` (e.g., `/page?group=foo`) would never show as active since matching compared the full `href` string against `location.pathname` which never includes query params. + + **Affected components:** Tabs, PluginHeader + +- 36987db: Fixed handling of the `style` prop on `Button`, `ButtonIcon`, and `ButtonLink` so that it is now correctly forwarded to the underlying element instead of being silently dropped. + + **Affected components:** Button, ButtonIcon, ButtonLink + +- 95702ab: Fixed Link variant default from `'body'` to `'body-medium'` to match actual CSS selectors. The previous default did not correspond to a valid variant value. + + **Affected components:** Link + +- 9027b10: Fixed scroll overflow in Menu and Select popover content when constrained by viewport height. + + **Affected components:** Menu, Select + +- 4105a78: Merged the internal `PluginHeaderToolbar` component into `PluginHeader`, removing the separate component and its associated types (`PluginHeaderToolbarOwnProps`, `PluginHeaderToolbarProps`) and definition (`PluginHeaderToolbarDefinition`). This is an internal refactor with no changes to the public API of `PluginHeader`. + + **Affected components:** PluginHeader + +- b303857: Fixed `isRequired` prop not being passed to the underlying React Aria field components in TextField, SearchField, and PasswordField. Previously, `isRequired` was consumed locally for the secondary label text but never forwarded, which meant the input elements lacked `aria-required="true"` and React Aria's built-in required validation was not activated. + + **Affected components:** TextField, SearchField, PasswordField + +- cd3cb0f: Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. +- 36987db: Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + + **Affected components:** Alert, Container, DialogBody, FieldLabel + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## 0.12.1-next.0 + +### Patch Changes + +- a1f4bee: Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components. +- 8909359: Fixed focus-visible outline styles for Menu and Select components. + + **Affected components:** Menu, Select + +- 0f462f8: Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type. +- 8909359: Added proper cursor styles for RadioGroup items. + + **Affected components:** RadioGroup + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## 0.12.0 + +### Minor Changes + +- 46a9adc: **BREAKING**: Alert no longer accepts a `surface` prop + + The Alert component's background is now driven entirely by its `status` prop. The `surface` prop has been removed. + + ```diff + - + + + ``` + + **Affected components:** Alert + +- b63c25b: **BREAKING**: Removed gray scale tokens and renamed background surface tokens to neutral tokens + + The `--bui-gray-1` through `--bui-gray-8` tokens have been removed. The `--bui-bg-surface-*` and `--bui-bg-neutral-on-surface-*` tokens have been replaced by a unified `--bui-bg-neutral-*` scale. + + **Migration:** + + Replace surface tokens directly: + + ```diff + - background: var(--bui-bg-surface-0); + + background: var(--bui-bg-neutral-0); + ``` + + Replace on-surface tokens shifted by +1: + + ```diff + - background: var(--bui-bg-neutral-on-surface-0); + + background: var(--bui-bg-neutral-1); + ``` + + Replace gray tokens 1-4 with neutral equivalents (`--bui-gray-5` through `--bui-gray-8` have no direct replacement): + + ```diff + - background: var(--bui-gray-1); + + background: var(--bui-bg-neutral-1); + ``` + +- 7898df0: **BREAKING**: Replaced `Surface` / `onSurface` system with new provider/consumer background system + + The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer `bg` architecture. + + **Types:** + + - `ContainerBg` — `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'` + - `ProviderBg` — `ContainerBg | 'neutral-auto'` + + Consumer components (e.g. Button) inherit the parent's `bg` via `data-on-bg`, and CSS handles the visual step-up. See "Neutral level capping" below for details on how levels are bounded. + + **Hooks:** + + - `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no `bg` is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. + - `useBgConsumer()` — for consumer components. Returns the parent container's `bg` unchanged. + + **Component roles:** + + - **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. **Transparent by default** — they do _not_ auto-increment; pass `bg="neutral-auto"` explicitly if you want automatic neutral stepping. + - **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit the parent container's `bg` unchanged. + - **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Card passes `bg="neutral-auto"` to its inner Box, so it auto-increments from the parent context. + + **Neutral level capping:** + + Provider components cap at `neutral-3`. There is no `neutral-4` prop value. The `neutral-4` level exists only in consumer component CSS — for example, a Button sitting on a `neutral-3` surface uses `neutral-4` tokens internally via `data-on-bg`. + + **Migration:** + + Rename the `surface` prop to `bg` on provider components and update values: + + ```diff + - + + + + - + + + + - + + + + - + + + ``` + + Remove `onSurface` from consumer components — they now always inherit from the parent container: + + ```diff + - + + + ``` + + **Affected components:** Button + +- 110fec0: **BREAKING**: Removed link and tint color tokens, added new status foreground tokens, and improved Link component styling + + The following color tokens have been removed: + + - `--bui-fg-link` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-fg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-bg-tint` (and all related tokens: `-hover`, `-pressed`, `-disabled`) + - `--bui-border-tint` (and all related tokens) + + **New Status Tokens:** + + Added dedicated tokens for status colors that distinguish between usage on status backgrounds vs. standalone usage: + + - `--bui-fg-danger-on-bg` / `--bui-fg-danger` + - `--bui-fg-warning-on-bg` / `--bui-fg-warning` + - `--bui-fg-success-on-bg` / `--bui-fg-success` + - `--bui-fg-info-on-bg` / `--bui-fg-info` + + The `-on-bg` variants are designed for text on colored backgrounds, while the base variants are for standalone status indicators with improved visibility and contrast. + + **Migration:** + + For link colors, migrate to one of the following alternatives: + + ```diff + .custom-link { + - color: var(--bui-fg-link); + + color: var(--bui-fg-info); /* For informational links */ + + /* or */ + + color: var(--bui-fg-primary); /* For standard text links */ + } + ``` + + For tint colors (backgrounds, foregrounds, borders), migrate to appropriate status or neutral colors: + + ```diff + .info-section { + - background: var(--bui-bg-tint); + + background: var(--bui-bg-info); /* For informational sections */ + + /* or */ + + background: var(--bui-bg-neutral-1); /* For neutral emphasis */ + } + ``` + + If you're using status foreground colors on colored backgrounds, update to the new `-on-bg` tokens: + + ```diff + .error-badge { + - color: var(--bui-fg-danger); + + color: var(--bui-fg-danger-on-bg); + background: var(--bui-bg-danger); + } + ``` + + **Affected components:** Link + +### Patch Changes + +- 644e303: Added a new `FullPage` component that fills the remaining viewport height below the `PluginHeader`. + + ```tsx + + + {/* content fills remaining height */} + + ``` + + **Affected components:** FullPage + +- 44877e4: Fixed dark theme `--bui-fg-secondary` and `--bui-fg-disabled` tokens using black-based `oklch(0% ...)` instead of white-based `oklch(100% ...)`, making secondary and disabled text visible on dark backgrounds. +- 350c948: Fixed Box component to forward HTML attributes to the underlying div element. + + **Affected components:** Box + +- 7455dae: Use node prefix on native imports +- c8ae765: Fixed nested Accordion icon state issue where the inner accordion's arrow icon would incorrectly show as expanded when only the outer accordion was expanded. The CSS selector now uses a direct parent selector to ensure the icon only responds to its own accordion's expanded state. + + Affected components: Accordion + +- 4d1b7f4: Fixed CSS Module syntax to comply with Next.js 16 Turbopack validation by flattening nested dark theme selectors. + + **Affected components:** Popover, Tooltip + +- 2c219b9: Added `destructive` prop to Button for dangerous actions like delete or remove. Works with all variants (primary, secondary, tertiary). + + **Affected components:** Button + +- 5af9e14: Fixed `useDefinition` hook adding literal "undefined" class name when no className prop was passed. +- 5c76d13: Allow `ref` as a prop on the `Tag` component + + Affected components: Tag + +- ab25658: Cleaned up `useDefinition` `ownProps` types to remove never-typed ghost properties from autocomplete. +- 741a98d: Allow data to be passed directly to the `useTable` hook using the property `data` instead of `getData()` for mode `"complete"`. + + This simplifies usage as data changes, rather than having to perform a `useEffect` when data changes, and then reloading the data. It also happens immediately, so stale data won't remain until a rerender (with an internal async state change), so less flickering. + + Affected components: Table + +- a0fe1b2: Fixed changing columns after first render from crashing. It now renders the table with the new column layout as columns change. + + Affected components: Table + +- 508bd1a: Added new `Alert` component with support for status variants (info, success, warning, danger), icons, loading states, and custom actions. + + Updated status color tokens for improved contrast and consistency across light and dark themes: + + - Added new `--bui-bg-info` and `--bui-fg-info` tokens for info status + - Updated `--bui-bg-danger`, `--bui-fg-danger` tokens + - Updated `--bui-bg-warning`, `--bui-fg-warning` tokens + - Updated `--bui-bg-success`, `--bui-fg-success` tokens + + **Affected components**: Alert + +- da30862: Fixed client-side navigation for container components by wrapping the container (not individual items) in RouterProvider. Components now conditionally provide routing context only when children have internal links, removing the Router context requirement when not needed. This also removes the need to wrap these components in MemoryRouter during tests when they are not using the `href` prop. + + Additionally, when multiple tabs match the current URL via prefix matching, the tab with the most specific path (highest segment count) is now selected. For example, with URL `/catalog/users/john`, a tab with path `/catalog/users` is now selected over a tab with path `/catalog`. + + Affected components: Tabs, Tab, TagGroup, Tag, Menu, MenuItem, MenuAutocomplete + +- 092c453: Fixed an infinite render loop in Tabs when navigating to a URL that doesn't match any tab `href`. +- becf851: export PasswordField component +- becee36: Migrated Accordion components to use `useDefinition` instead of `useStyles`, and added automatic background adaptation based on parent container context. +- 5320aa8: Fixed components to not require a Router context when rendering without internal links. + + Affected components: Link, ButtonLink, Row + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 8c39412: The Table component now wraps the react-aria-components `Table` with a `ResizableTableContainer` only if any column has a width property set. This means that column widths can adapt to the content otherwise (if no width is explicitly set). + + Affected components: Table + +- cb090b4: Bump react-aria-components to v1.14.0 +- c429101: Fixed React 17 compatibility by using `useId` from `react-aria` instead of the built-in React hook which is only available in React 18+. +- 74c5a76: Fixed Switch component disabled state styling to show `not-allowed` cursor and disabled text color. + + **Affected components:** Switch + +- 20131c5: Migrated to use the standard `backstage-cli package build` for CSS bundling instead of a custom build script. +- Updated dependencies + - @backstage/version-bridge@1.0.12 + ## 0.12.0-next.2 ### Minor Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 671be132e8..a1df4bcf8a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.12.0-next.2", + "version": "0.13.0-next.2", "backstage": { "role": "web-library" }, @@ -50,15 +50,17 @@ "@remixicon/react": "^4.6.0", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", - "react-aria-components": "^1.14.0" + "react-aria-components": "^1.14.0", + "use-sync-external-store": "^1.4.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "@types/use-sync-external-store": "^0.0.6", "eslint-plugin-storybook": "^10.3.0-alpha.1", "glob": "^11.0.1", - "globals": "^15.11.0", + "globals": "^17.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.30.2", diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index c80da62db9..0b11df6055 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -5,22 +5,24 @@ ```ts import type { ButtonProps as ButtonProps_2 } from 'react-aria-components'; import { CellProps as CellProps_2 } from 'react-aria-components'; -import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components'; +import type { CheckboxProps as CheckboxProps_2 } from 'react-aria-components'; import { ColumnProps as ColumnProps_2 } from 'react-aria-components'; import type { ColumnSize } from '@react-types/table'; import type { ColumnStaticSize } from '@react-types/table'; -import { ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; +import type { ComponentPropsWithoutRef } from 'react'; import type { ComponentPropsWithRef } from 'react'; import type { CSSProperties } from 'react'; -import { DetailedHTMLProps } from 'react'; import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components'; import type { DisclosureGroupProps } from 'react-aria-components'; import type { DisclosurePanelProps } from 'react-aria-components'; import type { DisclosureProps } from 'react-aria-components'; import type { ElementType } from 'react'; import { ForwardRefExoticComponent } from 'react'; +import type { GridListItemProps } from 'react-aria-components'; +import type { GridListProps } from 'react-aria-components'; import type { HeadingProps } from 'react-aria-components'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import type { LinkProps as LinkProps_2 } from 'react-aria-components'; import type { ListBoxItemProps } from 'react-aria-components'; @@ -36,15 +38,15 @@ import type { RadioProps as RadioProps_2 } from 'react-aria-components'; import type { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; -import { RowProps } from 'react-aria-components'; +import { RowProps as RowProps_2 } from 'react-aria-components'; import type { SearchFieldProps as SearchFieldProps_2 } from 'react-aria-components'; import type { SelectProps as SelectProps_2 } from 'react-aria-components'; import type { SeparatorProps } from 'react-aria-components'; import type { SortDescriptor as SortDescriptor_2 } from 'react-stately'; import type { SubmenuTriggerProps as SubmenuTriggerProps_2 } from 'react-aria-components'; import type { SwitchProps as SwitchProps_2 } from 'react-aria-components'; -import { TableBodyProps } from 'react-aria-components'; -import { TableHeaderProps } from 'react-aria-components'; +import { TableBodyProps as TableBodyProps_2 } from 'react-aria-components'; +import { TableHeaderProps as TableHeaderProps_2 } from 'react-aria-components'; import { TableProps as TableProps_2 } from 'react-aria-components'; import type { TabListProps as TabListProps_2 } from 'react-aria-components'; import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components'; @@ -56,7 +58,7 @@ import type { TagProps as TagProps_2 } from 'react-aria-components'; import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components'; import type { ToggleButtonGroupProps as ToggleButtonGroupProps_2 } from 'react-aria-components'; import type { ToggleButtonProps as ToggleButtonProps_2 } from 'react-aria-components'; -import { TooltipProps as TooltipProps_2 } from 'react-aria-components'; +import type { TooltipProps as TooltipProps_2 } from 'react-aria-components'; import { TooltipTriggerComponentProps } from 'react-aria-components'; // @public (undocumented) @@ -72,8 +74,13 @@ export const AccordionDefinition: { readonly classNames: { readonly root: 'bui-Accordion'; }; - readonly bg: 'consumer'; + readonly bg: 'provider'; readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + readonly default: 'neutral'; + }; + readonly children: {}; readonly className: {}; }; }; @@ -112,6 +119,8 @@ export interface AccordionGroupProps // @public export type AccordionOwnProps = { + bg?: ProviderBg; + children: ReactNode; className?: string; }; @@ -145,7 +154,7 @@ export interface AccordionPanelProps // @public export interface AccordionProps - extends Omit, + extends Omit, AccordionOwnProps {} // @public (undocumented) @@ -237,11 +246,34 @@ export type AlertOwnProps = { }; // @public -export interface AlertProps extends MarginProps, AlertOwnProps {} +export interface AlertProps + extends MarginProps, + AlertOwnProps, + Omit< + React.ComponentPropsWithoutRef<'div'>, + keyof AlertOwnProps | keyof MarginProps + > {} // @public (undocumented) export type AlignItems = 'stretch' | 'start' | 'center' | 'end'; +// @public +export type AnalyticsEventAttributes = { + [key: string]: string | boolean | number; +}; + +// @public +export type AnalyticsTracker = { + captureEvent: ( + action: string, + subject: string, + options?: { + value?: number; + attributes?: AnalyticsEventAttributes; + }, + ) => void; +}; + // @public (undocumented) export const Avatar: ForwardRefExoticComponent< AvatarProps & RefAttributes @@ -249,23 +281,41 @@ export const Avatar: ForwardRefExoticComponent< // @public export const AvatarDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-AvatarRoot'; readonly image: 'bui-AvatarImage'; readonly fallback: 'bui-AvatarFallback'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium', 'large']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'medium'; + }; + readonly purpose: { + readonly default: 'informative'; + }; + readonly src: {}; + readonly name: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> { - name: string; - purpose?: 'decoration' | 'informative'; - size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; +export type AvatarOwnProps = { src: string; -} + name: string; + size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; + purpose?: 'decoration' | 'informative'; + className?: string; +}; + +// @public (undocumented) +export interface AvatarProps + extends Omit, 'children' | 'className'>, + AvatarOwnProps {} // @public (undocumented) export interface BgContextValue { @@ -274,7 +324,7 @@ export interface BgContextValue { } // @public -export const BgProvider: ({ bg, children }: BgProviderProps) => JSX_2.Element; +export const BgProvider: (input: BgProviderProps) => JSX_2.Element; // @public (undocumented) export interface BgProviderProps { @@ -353,7 +403,7 @@ export const BoxDefinition: { export type BoxOwnProps = { as?: keyof JSX.IntrinsicElements; bg?: Responsive; - children?: ReactNode; + children: ReactNode; className?: string; style?: CSSProperties; }; @@ -363,7 +413,7 @@ export interface BoxProps extends SpaceProps, BoxOwnProps, BoxUtilityProps, - React.HTMLAttributes {} + Omit, 'children'> {} // @public (undocumented) export type BoxUtilityProps = { @@ -382,6 +432,15 @@ export type BoxUtilityProps = { // @public (undocumented) export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +// @public +export function BUIProvider(props: BUIProviderProps): JSX_2.Element; + +// @public (undocumented) +export type BUIProviderProps = { + useAnalytics?: UseAnalyticsFn; + children: ReactNode; +}; + // @public export const Button: ForwardRefExoticComponent< ButtonProps & RefAttributes @@ -417,7 +476,6 @@ export const ButtonDefinition: { readonly iconEnd: {}; readonly children: {}; readonly className: {}; - readonly style: {}; }; }; @@ -451,7 +509,6 @@ export const ButtonIconDefinition: { }; readonly icon: {}; readonly className: {}; - readonly style: {}; }; }; @@ -462,12 +519,11 @@ export type ButtonIconOwnProps = { icon?: ReactElement; loading?: boolean; className?: string; - style?: CSSProperties; }; // @public export interface ButtonIconProps - extends Omit, + extends Omit, ButtonIconOwnProps {} // @public (undocumented) @@ -485,7 +541,9 @@ export const ButtonLinkDefinition: { readonly content: 'bui-ButtonLinkContent'; }; readonly bg: 'consumer'; + readonly analytics: true; readonly propDefs: { + readonly noTrack: {}; readonly size: { readonly dataAttribute: true; readonly default: 'small'; @@ -498,24 +556,23 @@ export const ButtonLinkDefinition: { readonly iconEnd: {}; readonly children: {}; readonly className: {}; - readonly style: {}; }; }; // @public (undocumented) export type ButtonLinkOwnProps = { + noTrack?: boolean; size?: Responsive<'small' | 'medium'>; variant?: Responsive<'primary' | 'secondary' | 'tertiary'>; iconStart?: ReactElement; iconEnd?: ReactElement; children?: ReactNode; className?: string; - style?: CSSProperties; }; // @public export interface ButtonLinkProps - extends Omit, + extends Omit, ButtonLinkOwnProps {} // @public (undocumented) @@ -528,12 +585,11 @@ export type ButtonOwnProps = { loading?: boolean; children?: ReactNode; className?: string; - style?: CSSProperties; }; // @public export interface ButtonProps - extends Omit, + extends Omit, ButtonOwnProps {} // @public @@ -541,6 +597,12 @@ export const Card: ForwardRefExoticComponent< CardProps & RefAttributes >; +// @public (undocumented) +export type CardBaseProps = { + children?: ReactNode; + className?: string; +}; + // @public export const CardBody: ForwardRefExoticComponent< CardBodyProps & RefAttributes @@ -571,6 +633,16 @@ export interface CardBodyProps extends CardBodyOwnProps, React.HTMLAttributes {} +// @public (undocumented) +export type CardButtonVariant = { + onPress: NonNullable; + href?: never; + label: string; + target?: never; + rel?: never; + download?: never; +}; + // @public export const CardDefinition: { readonly styles: { @@ -578,10 +650,17 @@ export const CardDefinition: { }; readonly classNames: { readonly root: 'bui-Card'; + readonly trigger: 'bui-CardTrigger'; }; readonly propDefs: { readonly children: {}; readonly className: {}; + readonly onPress: {}; + readonly href: {}; + readonly label: {}; + readonly target: {}; + readonly rel: {}; + readonly download: {}; }; }; @@ -646,15 +725,42 @@ export interface CardHeaderProps React.HTMLAttributes {} // @public (undocumented) -export type CardOwnProps = { - children?: ReactNode; - className?: string; +export type CardLinkVariant = { + href: string; + onPress?: never; + label: string; + target?: string; + rel?: string; + download?: boolean | string; }; // @public -export interface CardProps - extends CardOwnProps, - React.HTMLAttributes {} +export type CardOwnProps = Pick< + CardBaseProps & (CardButtonVariant | CardLinkVariant | CardStaticVariant), + | 'children' + | 'className' + | 'onPress' + | 'href' + | 'label' + | 'target' + | 'rel' + | 'download' +>; + +// @public +export type CardProps = CardBaseProps & + Omit, 'onClick'> & + (CardButtonVariant | CardLinkVariant | CardStaticVariant); + +// @public (undocumented) +export type CardStaticVariant = { + onPress?: never; + href?: never; + label?: never; + target?: never; + rel?: never; + download?: never; +}; // @public (undocumented) export const Cell: { @@ -662,25 +768,33 @@ export const Cell: { displayName: string; }; +// @public +export type CellOwnProps = { + className?: string; +}; + // @public (undocumented) export const CellProfile: (props: CellProfileProps) => JSX_2.Element; -// @public (undocumented) -export interface CellProfileProps extends CellProps_2 { - // (undocumented) - color?: TextColors; - // (undocumented) - description?: string; - // (undocumented) - href?: string; - // (undocumented) - name?: string; - // (undocumented) +// @public +export type CellProfileOwnProps = { src?: string; -} + name?: string; + href?: string; + description?: string; + color?: TextColors; + className?: string; +}; -// @public (undocumented) -export interface CellProps extends CellProps_2 {} +// @public +export interface CellProfileProps + extends CellProfileOwnProps, + Omit {} + +// @public +export interface CellProps + extends CellOwnProps, + Omit {} // @public (undocumented) export const CellText: { @@ -688,19 +802,20 @@ export const CellText: { displayName: string; }; -// @public (undocumented) -export interface CellTextProps extends CellProps_2 { - // (undocumented) - color?: TextColors; - // (undocumented) - description?: string; - // (undocumented) - href?: string; - // (undocumented) - leadingIcon?: React.ReactNode | null; - // (undocumented) +// @public +export type CellTextOwnProps = { title: string; -} + description?: string; + color?: TextColors; + leadingIcon?: React.ReactNode | null; + href?: string; + className?: string; +}; + +// @public +export interface CellTextProps + extends CellTextOwnProps, + Omit {} // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -709,31 +824,35 @@ export const Checkbox: ForwardRefExoticComponent< // @public export const CheckboxDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Checkbox'; readonly indicator: 'bui-CheckboxIndicator'; }; - readonly dataAttributes: { - readonly selected: readonly [true, false]; - readonly indeterminate: readonly [true, false]; + readonly propDefs: { + readonly children: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface CheckboxProps extends CheckboxProps_2 { - // (undocumented) +export type CheckboxOwnProps = { children: React.ReactNode; -} + className?: string; +}; -// @public -export type ClassNamesMap = Record; +// @public (undocumented) +export interface CheckboxProps + extends Omit, + CheckboxOwnProps {} // @public (undocumented) export const Column: (props: ColumnProps) => JSX_2.Element; -// @public (undocumented) +// @public export interface ColumnConfig { - // (undocumented) cell: (item: T) => ReactElement; // (undocumented) defaultWidth?: ColumnSize | null; @@ -758,10 +877,15 @@ export interface ColumnConfig { } // @public (undocumented) -export interface ColumnProps extends Omit { - // (undocumented) +export type ColumnOwnProps = { children?: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface ColumnProps + extends ColumnOwnProps, + Omit {} // @public (undocumented) export type Columns = @@ -779,16 +903,6 @@ export type Columns = | '12' | 'auto'; -// @public -export interface ComponentDefinition { - // (undocumented) - classNames: ClassNamesMap; - // (undocumented) - dataAttributes?: DataAttributesMap; - // (undocumented) - utilityProps?: string[]; -} - // @public (undocumented) export const Container: ForwardRefExoticComponent< ContainerProps & RefAttributes @@ -805,18 +919,39 @@ export type ContainerBg = // @public export const ContainerDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Container'; }; - readonly utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display']; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ + 'my', + 'mt', + 'mb', + 'py', + 'pt', + 'pb', + 'display', + ]; }; // @public (undocumented) -export interface ContainerProps { - // (undocumented) +export type ContainerOwnProps = { children?: React.ReactNode; - // (undocumented) className?: string; + style?: React.CSSProperties; +}; + +// @public (undocumented) +export interface ContainerProps + extends ContainerOwnProps, + Omit, keyof ContainerOwnProps> { // (undocumented) mb?: SpaceProps['mb']; // (undocumented) @@ -829,8 +964,6 @@ export interface ContainerProps { pt?: SpaceProps['pt']; // (undocumented) py?: SpaceProps['py']; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) @@ -861,12 +994,6 @@ export interface CursorResponse { totalCount?: number; } -// @public -export type DataAttributesMap = Record; - -// @public -export type DataAttributeValues = readonly (string | number | boolean)[]; - // @public (undocumented) export const Dialog: ForwardRefExoticComponent< DialogProps & RefAttributes @@ -878,58 +1005,123 @@ export const DialogBody: ForwardRefExoticComponent< >; // @public -export interface DialogBodyProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export const DialogBodyDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogBody'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogBodyOwnProps = { + children?: ReactNode; className?: string; -} +}; + +// @public +export interface DialogBodyProps + extends DialogBodyOwnProps, + Omit, keyof DialogBodyOwnProps> {} // @public export const DialogDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly overlay: 'bui-DialogOverlay'; + readonly root: 'bui-DialogOverlay'; readonly dialog: 'bui-Dialog'; - readonly header: 'bui-DialogHeader'; - readonly headerTitle: 'bui-DialogHeaderTitle'; - readonly body: 'bui-DialogBody'; - readonly footer: 'bui-DialogFooter'; + readonly content: 'bui-DialogContent'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly width: {}; + readonly height: {}; + readonly style: {}; }; }; // @public (undocumented) export const DialogFooter: ForwardRefExoticComponent< - Omit< - DetailedHTMLProps, HTMLDivElement>, - 'ref' - > & - RefAttributes + DialogFooterProps & RefAttributes >; +// @public +export const DialogFooterDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogFooter'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogFooterOwnProps = { + children?: ReactNode; + className?: string; +}; + +// @public +export interface DialogFooterProps + extends DialogFooterOwnProps, + Omit, keyof DialogFooterOwnProps> {} + // @public (undocumented) export const DialogHeader: ForwardRefExoticComponent< DialogHeaderProps & RefAttributes >; // @public -export interface DialogHeaderProps extends HeadingProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export const DialogHeaderDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogHeader'; + readonly title: 'bui-DialogHeaderTitle'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogHeaderOwnProps = { + children?: ReactNode; className?: string; -} +}; // @public -export interface DialogProps extends ModalOverlayProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export interface DialogHeaderProps + extends DialogHeaderOwnProps, + Omit {} + +// @public (undocumented) +export type DialogOwnProps = { + children?: ReactNode; className?: string; - // (undocumented) - height?: number | string; - // (undocumented) width?: number | string; -} + height?: number | string; + style?: React.CSSProperties; +}; + +// @public +export interface DialogProps + extends DialogOwnProps, + Omit {} // @public (undocumented) export const DialogTrigger: (props: DialogTriggerProps) => JSX_2.Element; @@ -947,23 +1139,39 @@ export const FieldLabel: ForwardRefExoticComponent< // @public export const FieldLabelDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-FieldLabelWrapper'; readonly label: 'bui-FieldLabel'; readonly secondaryLabel: 'bui-FieldSecondaryLabel'; readonly description: 'bui-FieldDescription'; }; + readonly propDefs: { + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly htmlFor: {}; + readonly id: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type FieldLabelOwnProps = { + label?: string | null; + secondaryLabel?: string | null; + description?: string | null; + htmlFor?: string; + id?: string; + className?: string; }; // @public (undocumented) export interface FieldLabelProps - extends Pick, 'className'> { - description?: string | null; - htmlFor?: string; - id?: string; - label?: string | null; - secondaryLabel?: string | null; -} + extends FieldLabelOwnProps, + Omit, keyof FieldLabelOwnProps> {} // @public (undocumented) export interface FilterState { @@ -980,10 +1188,22 @@ export const Flex: ForwardRefExoticComponent< // @public export const FlexDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Flex'; }; - readonly utilityProps: [ + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ 'm', 'mb', 'ml', @@ -1003,39 +1223,32 @@ export const FlexDefinition: { 'justify', 'direction', ]; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; - }; }; // @public (undocumented) export type FlexDirection = 'row' | 'column'; // @public (undocumented) -export interface FlexProps extends SpaceProps { +export type FlexOwnProps = { + children: ReactNode; + className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface FlexProps + extends SpaceProps, + FlexOwnProps, + Omit, 'children'> { // (undocumented) align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>; // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) - className?: string; - // (undocumented) direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; // (undocumented) gap?: Responsive; // (undocumented) justify?: Responsive<'start' | 'center' | 'end' | 'between'>; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) @@ -1048,13 +1261,31 @@ export const FullPage: ForwardRefExoticComponent< // @public export const FullPageDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-FullPage'; }; + readonly propDefs: { + readonly className: {}; + }; +}; + +// @public (undocumented) +export type FullPageOwnProps = { + className?: string; }; // @public -export interface FullPageProps extends React.ComponentPropsWithoutRef<'main'> {} +export interface FullPageProps + extends Omit, 'className'>, + FullPageOwnProps {} + +// @public +export function getNodeText( + node: ReactNode | ((...args: any[]) => ReactNode), +): string | undefined; // @public (undocumented) export const Grid: { @@ -1066,10 +1297,22 @@ export const Grid: { // @public export const GridDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Grid'; }; - readonly utilityProps: [ + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ 'columns', 'gap', 'm', @@ -1087,44 +1330,40 @@ export const GridDefinition: { 'px', 'py', ]; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; - }; }; // @public export const GridItemDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-GridItem'; }; - readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan']; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; }; + readonly utilityProps: readonly ['colSpan', 'colEnd', 'colStart', 'rowSpan']; }; // @public (undocumented) -export interface GridItemProps { - // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export type GridItemOwnProps = { + children: ReactNode; className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface GridItemProps + extends GridItemOwnProps, + Omit, 'children'> { // (undocumented) colEnd?: Responsive; // (undocumented) @@ -1133,31 +1372,32 @@ export interface GridItemProps { colStart?: Responsive; // (undocumented) rowSpan?: Responsive; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) -export interface GridProps extends SpaceProps { - // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export type GridOwnProps = { + children: ReactNode; className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface GridProps + extends SpaceProps, + GridOwnProps, + Omit, 'children'> { // (undocumented) columns?: Responsive; // (undocumented) gap?: Responsive; - // (undocumented) - style?: React.CSSProperties; } // @public -export const HeaderPage: (props: HeaderPageProps) => JSX_2.Element; +export const Header: (props: HeaderProps) => JSX_2.Element; // @public -export interface HeaderPageBreadcrumb { +export interface HeaderBreadcrumb { // (undocumented) href: string; // (undocumented) @@ -1165,20 +1405,30 @@ export interface HeaderPageBreadcrumb { } // @public -export const HeaderPageDefinition: { +export const HeaderDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly root: 'bui-HeaderPage'; - readonly content: 'bui-HeaderPageContent'; - readonly breadcrumbs: 'bui-HeaderPageBreadcrumbs'; - readonly tabsWrapper: 'bui-HeaderPageTabsWrapper'; - readonly controls: 'bui-HeaderPageControls'; + readonly root: 'bui-Header'; + readonly content: 'bui-HeaderContent'; + readonly breadcrumbs: 'bui-HeaderBreadcrumbs'; + readonly tabsWrapper: 'bui-HeaderTabsWrapper'; + readonly controls: 'bui-HeaderControls'; + }; + readonly propDefs: { + readonly title: {}; + readonly customActions: {}; + readonly tabs: {}; + readonly breadcrumbs: {}; + readonly className: {}; }; }; // @public -export interface HeaderPageProps { +export interface HeaderOwnProps { // (undocumented) - breadcrumbs?: HeaderPageBreadcrumb[]; + breadcrumbs?: HeaderBreadcrumb[]; // (undocumented) className?: string; // (undocumented) @@ -1189,6 +1439,42 @@ export interface HeaderPageProps { title?: string; } +// @public @deprecated (undocumented) +export const HeaderPage: (props: HeaderProps) => JSX_2.Element; + +// @public @deprecated (undocumented) +export type HeaderPageBreadcrumb = HeaderBreadcrumb; + +// @public @deprecated (undocumented) +export const HeaderPageDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-Header'; + readonly content: 'bui-HeaderContent'; + readonly breadcrumbs: 'bui-HeaderBreadcrumbs'; + readonly tabsWrapper: 'bui-HeaderTabsWrapper'; + readonly controls: 'bui-HeaderControls'; + }; + readonly propDefs: { + readonly title: {}; + readonly customActions: {}; + readonly tabs: {}; + readonly breadcrumbs: {}; + readonly className: {}; + }; +}; + +// @public @deprecated (undocumented) +export type HeaderPageOwnProps = HeaderOwnProps; + +// @public @deprecated (undocumented) +export type HeaderPageProps = HeaderProps; + +// @public +export interface HeaderProps extends HeaderOwnProps {} + // @public export interface HeaderTab { // (undocumented) @@ -1216,45 +1502,133 @@ export const Link: ForwardRefExoticComponent< // @public export const LinkDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Link'; }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - readonly color: readonly [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - 'info', - ]; - readonly truncate: readonly [true, false]; - readonly standalone: readonly [true, false]; + readonly analytics: true; + readonly propDefs: { + readonly noTrack: {}; + readonly variant: { + readonly dataAttribute: true; + readonly default: 'body-medium'; + }; + readonly weight: { + readonly dataAttribute: true; + readonly default: 'regular'; + }; + readonly color: { + readonly dataAttribute: true; + readonly default: 'primary'; + }; + readonly truncate: { + readonly dataAttribute: true; + }; + readonly standalone: { + readonly dataAttribute: true; + }; + readonly title: {}; + readonly children: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface LinkProps extends LinkProps_2 { - // (undocumented) - children?: ReactNode; - // (undocumented) +export type LinkOwnProps = { + noTrack?: boolean; + variant?: TextVariants | Partial>; + weight?: TextWeights | Partial>; color?: | TextColors | TextColorStatus | Partial>; - // (undocumented) - standalone?: boolean; - // (undocumented) - title?: string; - // (undocumented) truncate?: boolean; - // (undocumented) - variant?: TextVariants | Partial>; - // (undocumented) - weight?: TextWeights | Partial>; -} + standalone?: boolean; + title?: string; + children?: ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface LinkProps + extends Omit, + LinkOwnProps {} + +// @public +export const List: (props: ListProps) => JSX_2.Element; + +// @public +export const ListDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-List'; + }; + readonly propDefs: { + readonly items: {}; + readonly children: {}; + readonly renderEmptyState: {}; + readonly className: {}; + }; +}; + +// @public +export type ListOwnProps = { + items?: GridListProps['items']; + children?: GridListProps['children']; + renderEmptyState?: GridListProps['renderEmptyState']; + className?: string; +}; + +// @public +export interface ListProps + extends ListOwnProps, + Omit, keyof ListOwnProps> {} + +// @public +export const ListRow: (props: ListRowProps) => JSX_2.Element; + +// @public +export const ListRowDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly bg: 'consumer'; + readonly classNames: { + readonly root: 'bui-ListRow'; + readonly check: 'bui-ListRowCheck'; + readonly icon: 'bui-ListRowIcon'; + readonly label: 'bui-ListRowLabel'; + readonly description: 'bui-ListRowDescription'; + readonly actions: 'bui-ListRowActions'; + }; + readonly propDefs: { + readonly children: {}; + readonly description: {}; + readonly icon: {}; + readonly menuItems: {}; + readonly customActions: {}; + readonly className: {}; + }; +}; + +// @public +export type ListRowOwnProps = { + children?: React.ReactNode; + description?: string; + icon?: React.ReactElement; + menuItems?: React.ReactNode; + customActions?: React.ReactNode; + className?: string; +}; + +// @public +export interface ListRowProps + extends ListRowOwnProps, + Omit {} // @public (undocumented) export interface MarginProps { @@ -1288,56 +1662,47 @@ export const MenuAutocompleteListbox: ( ) => JSX_2.Element; // @public (undocumented) -export interface MenuAutocompleteListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) +export type MenuAutocompleteListBoxOwnProps = MenuPopoverOwnProps & { placeholder?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + selectionMode?: ListBoxProps['selectionMode']; +}; + +// @public (undocumented) +export interface MenuAutocompleteListBoxProps + extends MenuAutocompleteListBoxOwnProps, + Omit, keyof MenuAutocompleteListBoxOwnProps> {} + +// @public (undocumented) +export type MenuAutocompleteOwnProps = MenuPopoverOwnProps & { + placeholder?: string; +}; // @public (undocumented) export interface MenuAutocompleteProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placeholder?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuAutocompleteOwnProps, + Omit, keyof MenuAutocompleteOwnProps> {} // @public export const MenuDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly root: 'bui-Menu'; - readonly popover: 'bui-MenuPopover'; + readonly root: 'bui-MenuPopover'; + readonly inner: 'bui-MenuInner'; readonly content: 'bui-MenuContent'; - readonly section: 'bui-MenuSection'; - readonly sectionHeader: 'bui-MenuSectionHeader'; - readonly item: 'bui-MenuItem'; - readonly itemListBox: 'bui-MenuItemListBox'; - readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck'; - readonly itemWrapper: 'bui-MenuItemWrapper'; - readonly itemContent: 'bui-MenuItemContent'; - readonly itemArrow: 'bui-MenuItemArrow'; - readonly separator: 'bui-MenuSeparator'; - readonly searchField: 'bui-MenuSearchField'; - readonly searchFieldInput: 'bui-MenuSearchFieldInput'; - readonly searchFieldClear: 'bui-MenuSearchFieldClear'; - readonly emptyState: 'bui-MenuEmptyState'; + }; + readonly propDefs: { + readonly placement: { + readonly default: 'bottom start'; + }; + readonly virtualized: { + readonly default: false; + }; + readonly maxWidth: {}; + readonly maxHeight: {}; + readonly style: {}; + readonly className: {}; }; }; @@ -1345,16 +1710,19 @@ export const MenuDefinition: { export const MenuItem: (props: MenuItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuItemProps - extends MenuItemProps_2, - Omit { - // (undocumented) - children: React.ReactNode; - // (undocumented) - color?: 'primary' | 'danger'; - // (undocumented) +export type MenuItemOwnProps = { iconStart?: React.ReactNode; -} + children: React.ReactNode; + color?: 'primary' | 'danger'; + href?: MenuItemProps_2['href']; + noTrack?: boolean; + className?: string; +}; + +// @public (undocumented) +export interface MenuItemProps + extends MenuItemOwnProps, + Omit {} // @public (undocumented) export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; @@ -1363,59 +1731,71 @@ export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; export const MenuListBoxItem: (props: MenuListBoxItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuListBoxItemProps - extends ListBoxItemProps, - Omit { - // (undocumented) +export type MenuListBoxItemOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface MenuListBoxItemProps + extends MenuListBoxItemOwnProps, + Omit {} + +// @public (undocumented) +export type MenuListBoxOwnProps = MenuPopoverOwnProps & { + selectionMode?: ListBoxProps['selectionMode']; +}; // @public (undocumented) export interface MenuListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) + extends MenuListBoxOwnProps, + Omit, keyof MenuListBoxOwnProps> {} + +// @public (undocumented) +export type MenuOwnProps = MenuPopoverOwnProps; + +// @public +export type MenuPopoverOwnProps = { placement?: PopoverProps_2['placement']; - // (undocumented) virtualized?: boolean; -} + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; // @public (undocumented) export interface MenuProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuOwnProps, + Omit, keyof MenuOwnProps> {} // @public (undocumented) export const MenuSection: (props: MenuSectionProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSectionProps - extends MenuSectionProps_2, - Omit, 'children'> { - // (undocumented) - children: React.ReactNode; - // (undocumented) +export type MenuSectionOwnProps = { title: string; -} + children: React.ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface MenuSectionProps + extends MenuSectionOwnProps, + Omit, keyof MenuSectionOwnProps> {} // @public (undocumented) export const MenuSeparator: (props: MenuSeparatorProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSeparatorProps extends SeparatorProps {} +export type MenuSeparatorOwnProps = { + className?: string; +}; + +// @public (undocumented) +export interface MenuSeparatorProps + extends MenuSeparatorOwnProps, + Omit {} // @public (undocumented) export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element; @@ -1518,6 +1898,9 @@ export const PasswordField: ForwardRefExoticComponent< // @public export const PasswordFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-PasswordField'; readonly inputWrapper: 'bui-PasswordFieldInputWrapper'; @@ -1525,25 +1908,44 @@ export const PasswordFieldDefinition: { readonly inputIcon: 'bui-PasswordFieldIcon'; readonly inputVisibility: 'bui-PasswordFieldVisibility'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: {}; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface PasswordFieldProps - extends TextFieldProps_2, - Omit { +export type PasswordFieldOwnProps = { + size?: 'small' | 'medium' | Partial>; + className?: string; icon?: ReactNode; placeholder?: string; - size?: 'small' | 'medium' | Partial>; -} + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface PasswordFieldProps + extends Omit, + PasswordFieldOwnProps {} // @public export const PluginHeader: (props: PluginHeaderProps) => JSX_2.Element; // @public export const PluginHeaderDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-PluginHeader'; readonly toolbar: 'bui-PluginHeaderToolbar'; @@ -1552,12 +1954,21 @@ export const PluginHeaderDefinition: { readonly toolbarControls: 'bui-PluginHeaderToolbarControls'; readonly toolbarIcon: 'bui-PluginHeaderToolbarIcon'; readonly toolbarName: 'bui-PluginHeaderToolbarName'; - readonly tabsWrapper: 'bui-PluginHeaderTabsWrapper'; + readonly tabs: 'bui-PluginHeaderTabsWrapper'; + }; + readonly propDefs: { + readonly icon: {}; + readonly title: {}; + readonly titleLink: {}; + readonly customActions: {}; + readonly tabs: {}; + readonly onTabSelectionChange: {}; + readonly className: {}; }; }; // @public -export interface PluginHeaderProps { +export interface PluginHeaderOwnProps { // (undocumented) className?: string; // (undocumented) @@ -1574,6 +1985,9 @@ export interface PluginHeaderProps { titleLink?: string; } +// @public +export interface PluginHeaderProps extends PluginHeaderOwnProps {} + // @public export const Popover: ForwardRefExoticComponent< PopoverProps & RefAttributes @@ -1581,21 +1995,35 @@ export const Popover: ForwardRefExoticComponent< // @public export const PopoverDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Popover'; readonly arrow: 'bui-PopoverArrow'; readonly content: 'bui-PopoverContent'; }; + readonly propDefs: { + readonly children: {}; + readonly hideArrow: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type PopoverOwnProps = { + children: React.ReactNode; + hideArrow?: boolean; + className?: string; }; // @public -export interface PopoverProps extends Omit { - children: React.ReactNode; - hideArrow?: boolean; -} +export interface PopoverProps + extends Omit, + PopoverOwnProps {} // @public -export type ProviderBg = ContainerBg | 'neutral-auto'; +export type ProviderBg = 'neutral' | 'danger' | 'warning' | 'success'; // @public (undocumented) export interface QueryOptions { @@ -1624,6 +2052,19 @@ export const Radio: ForwardRefExoticComponent< RadioProps & RefAttributes >; +// @public +export const RadioDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-Radio'; + }; + readonly propDefs: { + readonly className: {}; + }; +}; + // @public (undocumented) export const RadioGroup: ForwardRefExoticComponent< RadioGroupProps & RefAttributes @@ -1631,23 +2072,47 @@ export const RadioGroup: ForwardRefExoticComponent< // @public export const RadioGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-RadioGroup'; readonly content: 'bui-RadioGroupContent'; - readonly radio: 'bui-Radio'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly isRequired: {}; }; }; // @public (undocumented) -export interface RadioGroupProps - extends Omit, - Omit { - // (undocumented) +export type RadioGroupOwnProps = { children?: ReactNode; -} + className?: string; + label?: FieldLabelProps['label']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; + description?: FieldLabelProps['description']; + isRequired?: RadioGroupProps_2['isRequired']; +}; // @public (undocumented) -export interface RadioProps extends RadioProps_2 {} +export interface RadioGroupProps + extends RadioGroupOwnProps, + Omit {} + +// @public (undocumented) +export type RadioOwnProps = { + className?: string; +}; + +// @public (undocumented) +export interface RadioProps + extends RadioOwnProps, + Omit {} // @public (undocumented) export type Responsive = T | Partial>; @@ -1666,11 +2131,103 @@ export interface RowConfig { } // @public (undocumented) +export type RowOwnProps = { + columns?: RowProps_2['columns']; + children?: RowProps_2['children']; + href?: string; + noTrack?: boolean; +}; + +// @public (undocumented) +export interface RowProps + extends RowOwnProps, + Omit, keyof RowOwnProps> {} + +// @public export type RowRenderFn = (params: { item: T; index: number; }) => ReactNode; +// @public (undocumented) +export function SearchAutocomplete( + props: SearchAutocompleteProps, +): JSX_2.Element; + +// @public +export const SearchAutocompleteDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-SearchAutocomplete'; + readonly searchField: 'bui-SearchAutocompleteSearchField'; + readonly searchFieldInput: 'bui-SearchAutocompleteInput'; + readonly searchFieldClear: 'bui-SearchAutocompleteClear'; + readonly popover: 'bui-SearchAutocompletePopover'; + readonly inner: 'bui-SearchAutocompleteInner'; + readonly listBox: 'bui-SearchAutocompleteListBox'; + readonly loadingState: 'bui-SearchAutocompleteLoadingState'; + readonly emptyState: 'bui-SearchAutocompleteEmptyState'; + }; + readonly propDefs: { + readonly 'aria-label': {}; + readonly 'aria-labelledby': {}; + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly placeholder: { + readonly default: 'Search'; + }; + readonly inputValue: {}; + readonly onInputChange: {}; + readonly popoverWidth: {}; + readonly popoverPlacement: {}; + readonly children: {}; + readonly isLoading: {}; + readonly defaultOpen: {}; + readonly className: {}; + readonly style: {}; + }; +}; + +// @public (undocumented) +export function SearchAutocompleteItem( + props: SearchAutocompleteItemProps, +): JSX_2.Element; + +// @public (undocumented) +export type SearchAutocompleteItemOwnProps = { + children: ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface SearchAutocompleteItemProps + extends SearchAutocompleteItemOwnProps, + Omit {} + +// @public (undocumented) +export type SearchAutocompleteOwnProps = { + inputValue?: string; + onInputChange?: (value: string) => void; + size?: 'small' | 'medium' | Partial>; + 'aria-label'?: string; + 'aria-labelledby'?: string; + placeholder?: string; + popoverWidth?: string; + popoverPlacement?: PopoverProps_2['placement']; + children?: ReactNode; + isLoading?: boolean; + defaultOpen?: boolean; + className?: string; + style?: React.CSSProperties; +}; + +// @public (undocumented) +export interface SearchAutocompleteProps extends SearchAutocompleteOwnProps {} + // @public (undocumented) export const SearchField: ForwardRefExoticComponent< SearchFieldProps & RefAttributes @@ -1678,6 +2235,9 @@ export const SearchField: ForwardRefExoticComponent< // @public export const SearchFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-SearchField'; readonly clear: 'bui-SearchFieldClear'; @@ -1685,21 +2245,43 @@ export const SearchFieldDefinition: { readonly input: 'bui-SearchFieldInput'; readonly inputIcon: 'bui-SearchFieldInputIcon'; }; - readonly dataAttributes: { - readonly startCollapsed: readonly [true, false]; - readonly size: readonly ['small', 'medium']; + readonly bg: 'consumer'; + readonly propDefs: { + readonly startCollapsed: { + readonly dataAttribute: true; + readonly default: false; + }; + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: { + readonly default: 'Search'; + }; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface SearchFieldProps - extends SearchFieldProps_2, - Omit { +export type SearchFieldOwnProps = { icon?: ReactNode | false; - placeholder?: string; size?: 'small' | 'medium' | Partial>; + placeholder?: string; startCollapsed?: boolean; -} + className?: string; + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface SearchFieldProps + extends Omit, + SearchFieldOwnProps {} // @public (undocumented) export interface SearchState { @@ -1716,36 +2298,49 @@ export const Select: ForwardRefExoticComponent< // @public export const SelectDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Select'; readonly popover: 'bui-SelectPopover'; - readonly trigger: 'bui-SelectTrigger'; - readonly chevron: 'bui-SelectTriggerChevron'; - readonly value: 'bui-SelectValue'; - readonly list: 'bui-SelectList'; - readonly item: 'bui-SelectItem'; - readonly itemIndicator: 'bui-SelectItemIndicator'; - readonly itemLabel: 'bui-SelectItemLabel'; - readonly searchWrapper: 'bui-SelectSearchWrapper'; - readonly search: 'bui-SelectSearch'; - readonly searchClear: 'bui-SelectSearchClear'; - readonly noResults: 'bui-SelectNoResults'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly icon: {}; + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly options: {}; + readonly searchable: {}; + readonly searchPlaceholder: {}; + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly isRequired: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface SelectProps - extends SelectProps_2, - Omit { +export type SelectOwnProps = { icon?: ReactNode; + size?: 'small' | 'medium' | Partial>; options?: Array; searchable?: boolean; searchPlaceholder?: string; + label?: FieldLabelProps['label']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; + description?: FieldLabelProps['description']; + isRequired?: boolean; + className?: string; +}; + +// @public (undocumented) +export interface SelectProps + extends SelectOwnProps, + Omit, keyof SelectOwnProps> { selectionMode?: T; - size?: 'small' | 'medium' | Partial>; } // @public (undocumented) @@ -1753,20 +2348,41 @@ export const Skeleton: (props: SkeletonProps) => JSX_2.Element; // @public export const SkeletonDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Skeleton'; }; + readonly propDefs: { + readonly width: { + readonly default: 80; + }; + readonly height: { + readonly default: 24; + }; + readonly rounded: { + readonly dataAttribute: true; + readonly default: false; + }; + readonly className: {}; + readonly style: {}; + }; }; // @public (undocumented) -export interface SkeletonProps extends ComponentProps<'div'> { - // (undocumented) - height?: number | string; - // (undocumented) - rounded?: boolean; - // (undocumented) +export type SkeletonOwnProps = { width?: number | string; -} + height?: number | string; + rounded?: boolean; + className?: string; + style?: React.CSSProperties; +}; + +// @public (undocumented) +export interface SkeletonProps + extends Omit, 'children' | 'className' | 'style'>, + SkeletonOwnProps {} // @public (undocumented) export type SortDescriptor = SortDescriptor_2; @@ -1815,66 +2431,64 @@ export const Switch: ForwardRefExoticComponent< // @public export const SwitchDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Switch'; readonly indicator: 'bui-SwitchIndicator'; }; + readonly propDefs: { + readonly label: {}; + readonly className: {}; + }; }; // @public (undocumented) -export interface SwitchProps extends SwitchProps_2 { +export type SwitchOwnProps = { label?: string; -} + className?: string; +}; + +// @public (undocumented) +export interface SwitchProps + extends Omit, + SwitchOwnProps {} // @public export const Tab: (props: TabProps) => JSX_2.Element; // @public (undocumented) -export function Table({ - columnConfig, - data, - loading, - isStale, - error, - pagination, - sort, - rowConfig, - selection, - emptyState, - className, - style, -}: TableProps): JSX_2.Element; +export function Table(input: TableProps): JSX_2.Element; // @public (undocumented) export const TableBody: ( props: TableBodyProps, ) => JSX_2.Element; +// @public (undocumented) +export type TableBodyOwnProps = {}; + +// @public (undocumented) +export interface TableBodyProps + extends TableBodyOwnProps, + Omit, keyof TableBodyOwnProps> {} + // @public export const TableDefinition: { - readonly classNames: { - readonly table: 'bui-Table'; - readonly header: 'bui-TableHeader'; - readonly body: 'bui-TableBody'; - readonly row: 'bui-TableRow'; - readonly head: 'bui-TableHead'; - readonly headContent: 'bui-TableHeadContent'; - readonly headSortButton: 'bui-TableHeadSortButton'; - readonly caption: 'bui-TableCaption'; - readonly cell: 'bui-TableCell'; - readonly cellContentWrapper: 'bui-TableCellContentWrapper'; - readonly cellContent: 'bui-TableCellContent'; - readonly cellIcon: 'bui-TableCellIcon'; - readonly cellProfileAvatar: 'bui-TableCellProfileAvatar'; - readonly cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage'; - readonly cellProfileAvatarFallback: 'bui-TableCellProfileAvatarFallback'; - readonly cellProfileName: 'bui-TableCellProfileName'; - readonly cellProfileLink: 'bui-TableCellProfileLink'; - readonly headSelection: 'bui-TableHeadSelection'; - readonly cellSelection: 'bui-TableCellSelection'; + readonly styles: { + readonly [key: string]: string; }; - readonly dataAttributes: { - readonly stale: readonly [true, false]; + readonly classNames: { + readonly root: 'bui-Table'; + }; + readonly propDefs: { + readonly stale: { + readonly dataAttribute: true; + }; + readonly loading: { + readonly dataAttribute: true; + }; }; }; @@ -1883,6 +2497,17 @@ export const TableHeader: ( props: TableHeaderProps, ) => JSX_2.Element; +// @public (undocumented) +export type TableHeaderOwnProps = { + columns?: TableHeaderProps_2['columns']; + children?: TableHeaderProps_2['children']; +}; + +// @public (undocumented) +export interface TableHeaderProps + extends TableHeaderOwnProps, + Omit, keyof TableHeaderOwnProps> {} + // @public (undocumented) export interface TableItem { // (undocumented) @@ -1890,59 +2515,59 @@ export interface TableItem { } // @public -export function TablePagination({ - pageSize, - pageSizeOptions, - offset, - totalCount, - hasNextPage, - hasPreviousPage, - onNextPage, - onPreviousPage, - onPageSizeChange, - showPageSizeOptions, - getLabel, -}: TablePaginationProps): JSX_2.Element; +export function TablePagination(props: TablePaginationProps): JSX_2.Element; // @public export const TablePaginationDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-TablePagination'; readonly left: 'bui-TablePaginationLeft'; readonly right: 'bui-TablePaginationRight'; readonly select: 'bui-TablePaginationSelect'; }; + readonly propDefs: { + readonly pageSize: {}; + readonly pageSizeOptions: { + readonly default: PageSizeOption[]; + }; + readonly offset: {}; + readonly totalCount: {}; + readonly hasNextPage: {}; + readonly hasPreviousPage: {}; + readonly onNextPage: {}; + readonly onPreviousPage: {}; + readonly onPageSizeChange: {}; + readonly showPageSizeOptions: { + readonly default: true; + }; + readonly getLabel: {}; + }; }; // @public (undocumented) -export interface TablePaginationProps { - // (undocumented) +export type TablePaginationOwnProps = { + pageSize: number; + pageSizeOptions?: number[] | PageSizeOption[]; + offset?: number; + totalCount?: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + onNextPage: () => void; + onPreviousPage: () => void; + onPageSizeChange?: (size: number) => void; + showPageSizeOptions?: boolean; getLabel?: (params: { pageSize: number; offset?: number; totalCount?: number; }) => string; - // (undocumented) - hasNextPage: boolean; - // (undocumented) - hasPreviousPage: boolean; - // (undocumented) - offset?: number; - // (undocumented) - onNextPage: () => void; - // (undocumented) - onPageSizeChange?: (size: number) => void; - // (undocumented) - onPreviousPage: () => void; - // (undocumented) - pageSize: number; - // (undocumented) - pageSizeOptions?: number[] | PageSizeOption[]; - // (undocumented) - showPageSizeOptions?: boolean; - // (undocumented) - totalCount?: number; -} +}; + +// @public (undocumented) +export interface TablePaginationProps extends TablePaginationOwnProps {} // @public (undocumented) export type TablePaginationType = NoPagination | PagePagination; @@ -1973,16 +2598,23 @@ export interface TableProps { sort?: SortState; // (undocumented) style?: React.CSSProperties; + // (undocumented) + virtualized?: VirtualizedProp; } // @public (undocumented) export const TableRoot: (props: TableRootProps) => JSX_2.Element; // @public (undocumented) -export interface TableRootProps extends TableProps_2 { - // (undocumented) +export type TableRootOwnProps = { stale?: boolean; -} + loading?: boolean; +}; + +// @public (undocumented) +export interface TableRootProps + extends TableRootOwnProps, + Omit {} // @public (undocumented) export interface TableSelection { @@ -2000,40 +2632,73 @@ export interface TableSelection { export const TabList: (props: TabListProps) => JSX_2.Element; // @public -export interface TabListProps extends Omit, 'items'> {} +export type TabListOwnProps = { + className?: string; + children?: TabListProps_2['children']; +}; + +// @public +export interface TabListProps + extends TabListOwnProps, + Omit, 'items' | keyof TabListOwnProps> {} // @public export type TabMatchStrategy = 'exact' | 'prefix'; +// @public +export type TabOwnProps = { + className?: string; + matchStrategy?: TabMatchStrategy; + href?: TabProps_2['href']; + id?: TabProps_2['id']; + noTrack?: boolean; +}; + // @public export const TabPanel: (props: TabPanelProps) => JSX_2.Element; // @public -export interface TabPanelProps extends TabPanelProps_2 {} +export type TabPanelOwnProps = { + className?: string; +}; // @public -export interface TabProps extends TabProps_2 { - matchStrategy?: 'exact' | 'prefix'; -} +export interface TabPanelProps + extends TabPanelOwnProps, + Omit {} + +// @public +export interface TabProps + extends TabOwnProps, + Omit {} // @public export const Tabs: (props: TabsProps) => JSX_2.Element | null; // @public export const TabsDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly tabs: 'bui-Tabs'; - readonly tabList: 'bui-TabList'; - readonly tabListWrapper: 'bui-TabListWrapper'; - readonly tab: 'bui-Tab'; - readonly tabActive: 'bui-TabActive'; - readonly tabHovered: 'bui-TabHovered'; - readonly panel: 'bui-TabPanel'; + readonly root: 'bui-Tabs'; + }; + readonly propDefs: { + readonly className: {}; + readonly children: {}; }; }; // @public -export interface TabsProps extends TabsProps_2 {} +export type TabsOwnProps = { + className?: string; + children?: TabsProps_2['children']; +}; + +// @public +export interface TabsProps + extends TabsOwnProps, + Omit {} // @public export const Tag: ForwardRefExoticComponent< @@ -2047,25 +2712,48 @@ export const TagGroup: ( // @public export const TagGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly group: 'bui-TagGroup'; + readonly root: 'bui-TagGroup'; readonly list: 'bui-TagList'; - readonly tag: 'bui-Tag'; - readonly tagIcon: 'bui-TagIcon'; - readonly tagRemoveButton: 'bui-TagRemoveButton'; + }; + readonly propDefs: { + readonly items: {}; + readonly children: {}; + readonly renderEmptyState: {}; + readonly className: {}; }; }; // @public -export interface TagGroupProps - extends Omit, - Pick, 'items' | 'children' | 'renderEmptyState'> {} +export type TagGroupOwnProps = { + items?: TagListProps['items']; + children?: TagListProps['children']; + renderEmptyState?: TagListProps['renderEmptyState']; + className?: string; +}; // @public -export interface TagProps extends TagProps_2 { +export interface TagGroupProps + extends TagGroupOwnProps, + Omit {} + +// @public +export type TagOwnProps = { icon?: React.ReactNode; size?: 'small' | 'medium'; -} + href?: TagProps_2['href']; + children?: TagProps_2['children']; + className?: string; + noTrack?: boolean; +}; + +// @public +export interface TagProps + extends TagOwnProps, + Omit {} // @public (undocumented) const Text_2: { @@ -2086,21 +2774,32 @@ export type TextColorStatus = 'danger' | 'warning' | 'success' | 'info'; // @public export const TextDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Text'; }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - readonly color: readonly [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - 'info', - ]; - readonly truncate: readonly [true, false]; + readonly propDefs: { + readonly as: { + readonly default: 'span'; + }; + readonly variant: { + readonly dataAttribute: true; + readonly default: 'body-medium'; + }; + readonly weight: { + readonly dataAttribute: true; + readonly default: 'regular'; + }; + readonly color: { + readonly dataAttribute: true; + readonly default: 'primary'; + }; + readonly truncate: { + readonly dataAttribute: true; + }; + readonly className: {}; }; }; @@ -2111,6 +2810,9 @@ export const TextField: ForwardRefExoticComponent< // @public export const TextFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-TextField'; readonly inputWrapper: 'bui-InputWrapper'; @@ -2118,20 +2820,36 @@ export const TextFieldDefinition: { readonly inputIcon: 'bui-InputIcon'; readonly inputAction: 'bui-InputAction'; }; - readonly dataAttributes: { - readonly invalid: readonly [true, false]; - readonly disabled: readonly [true, false]; - readonly size: readonly ['small', 'medium']; + readonly bg: 'consumer'; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: {}; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface TextFieldProps - extends TextFieldProps_2, - Omit { +export type TextFieldOwnProps = { + size?: 'small' | 'medium' | Partial>; + className?: string; icon?: ReactNode; placeholder?: string; - size?: 'small' | 'medium' | Partial>; + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface TextFieldProps + extends Omit, + TextFieldOwnProps { type?: 'text' | 'email' | 'tel' | 'url'; } @@ -2159,6 +2877,7 @@ export type TextOwnProps = { | TextColorStatus | Partial>; truncate?: boolean; + className?: string; }; // @public (undocumented) @@ -2186,12 +2905,22 @@ export const ToggleButton: ForwardRefExoticComponent< // @public export const ToggleButtonDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-ToggleButton'; readonly content: 'bui-ToggleButtonContent'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly iconStart: {}; + readonly iconEnd: {}; + readonly children: {}; + readonly className: {}; }; }; @@ -2202,30 +2931,42 @@ export const ToggleButtonGroup: ForwardRefExoticComponent< // @public export const ToggleButtonGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-ToggleButtonGroup'; }; - readonly dataAttributes: { - readonly orientation: readonly ['horizontal', 'vertical']; + readonly propDefs: { + readonly className: {}; + readonly children: {}; }; }; +// @public (undocumented) +export type ToggleButtonGroupOwnProps = { + className?: string; + children?: ReactNode; +}; + // @public (undocumented) export interface ToggleButtonGroupProps - extends Omit { - // (undocumented) - orientation?: NonNullable; -} + extends Omit, + ToggleButtonGroupOwnProps {} + +// @public (undocumented) +export type ToggleButtonOwnProps = { + size?: 'small' | 'medium' | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ToggleButtonProps_2['children']; + className?: string; +}; // @public -export interface ToggleButtonProps extends ToggleButtonProps_2 { - // (undocumented) - iconEnd?: ReactElement; - // (undocumented) - iconStart?: ReactElement; - // (undocumented) - size?: 'small' | 'medium' | Partial>; -} +export interface ToggleButtonProps + extends Omit, + ToggleButtonOwnProps {} // @public (undocumented) export const Tooltip: ForwardRefExoticComponent< @@ -2234,23 +2975,42 @@ export const Tooltip: ForwardRefExoticComponent< // @public export const TooltipDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly tooltip: 'bui-Tooltip'; + readonly content: 'bui-TooltipContent'; readonly arrow: 'bui-TooltipArrow'; }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; }; // @public (undocumented) -export interface TooltipProps extends Omit { - // (undocumented) +export type TooltipOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface TooltipProps + extends Omit, + TooltipOwnProps {} // @public (undocumented) export const TooltipTrigger: ( props: TooltipTriggerComponentProps, ) => JSX_2.Element; +// @public +export function useAnalytics(): AnalyticsTracker; + +// @public +export type UseAnalyticsFn = () => AnalyticsTracker; + // @public export function useBgConsumer(): BgContextValue; @@ -2363,19 +3123,39 @@ export interface UtilityProps extends SpaceProps { rowSpan?: Responsive; } +// @public (undocumented) +export type VirtualizedProp = + | boolean + | { + rowHeight: number; + } + | { + estimatedRowHeight: number; + }; + // @public export const VisuallyHidden: (props: VisuallyHiddenProps) => JSX_2.Element; // @public export const VisuallyHiddenDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-VisuallyHidden'; }; + readonly propDefs: { + readonly className: {}; + }; +}; + +// @public (undocumented) +export type VisuallyHiddenOwnProps = { + className?: string; }; // @public -export interface VisuallyHiddenProps extends ComponentProps<'div'> { - // (undocumented) - children?: React.ReactNode; -} +export interface VisuallyHiddenProps + extends Omit, 'className'>, + VisuallyHiddenOwnProps {} ``` diff --git a/packages/ui/src/analytics/getNodeText.ts b/packages/ui/src/analytics/getNodeText.ts new file mode 100644 index 0000000000..a1a58c34b7 --- /dev/null +++ b/packages/ui/src/analytics/getNodeText.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2026 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 ReactNode, isValidElement, Children } from 'react'; + +/** + * Recursively extracts text content from a React node tree. + * Returns undefined if no text content is found (e.g. icon-only children + * or render functions). + * + * @public + */ +export function getNodeText( + node: ReactNode | ((...args: any[]) => ReactNode), +): string | undefined { + if (typeof node === 'function') { + return undefined; + } + if (Array.isArray(node)) { + const text = Children.map(node, getNodeText)?.filter(Boolean).join(' '); + return text || undefined; + } + if (isValidElement(node)) { + return getNodeText(node.props.children); + } + if (typeof node === 'string' || typeof node === 'number') { + return String(node); + } + return undefined; +} diff --git a/packages/ui/src/analytics/index.ts b/packages/ui/src/analytics/index.ts new file mode 100644 index 0000000000..de42ed3181 --- /dev/null +++ b/packages/ui/src/analytics/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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 { useAnalytics } from './useAnalytics'; +export { getNodeText } from './getNodeText'; +export type { + AnalyticsTracker, + AnalyticsEventAttributes, + UseAnalyticsFn, +} from './types'; diff --git a/packages/ui/src/analytics/types.ts b/packages/ui/src/analytics/types.ts new file mode 100644 index 0000000000..62b6eb6186 --- /dev/null +++ b/packages/ui/src/analytics/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2026 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. + */ + +/** + * Key-value metadata attached to analytics events. + * @public + */ +export type AnalyticsEventAttributes = { + [key: string]: string | boolean | number; +}; + +/** + * A generic interface for capturing analytics events. Consumers provide + * an implementation via `BUIProvider` — this allows `@backstage/ui` + * to fire analytics events without depending on any specific analytics + * system. The signature intentionally matches Backstage's own + * `AnalyticsTracker` so it can be wired through directly. + * @public + */ +export type AnalyticsTracker = { + captureEvent: ( + action: string, + subject: string, + options?: { + value?: number; + attributes?: AnalyticsEventAttributes; + }, + ) => void; +}; + +/** + * A hook function that returns an AnalyticsTracker. + * Provided via context by the consumer (e.g. a Backstage app). + * @public + */ +export type UseAnalyticsFn = () => AnalyticsTracker; diff --git a/packages/ui/src/analytics/useAnalytics.ts b/packages/ui/src/analytics/useAnalytics.ts new file mode 100644 index 0000000000..127b3aa4b6 --- /dev/null +++ b/packages/ui/src/analytics/useAnalytics.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2026 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 { useRef } from 'react'; +import { + createVersionedContext, + useVersionedContext, +} from '@backstage/version-bridge'; +import type { AnalyticsTracker, UseAnalyticsFn } from './types'; + +/** @internal */ +export const noopTracker: AnalyticsTracker = { + captureEvent: () => {}, +}; + +const noopUseAnalytics: UseAnalyticsFn = () => noopTracker; + +/** @internal */ +export type BUIContextValue = { + useAnalytics?: UseAnalyticsFn; +}; + +/** @internal */ +export const BUIContext = createVersionedContext<{ + 1: BUIContextValue; +}>('bui'); + +/** + * Returns an AnalyticsTracker for capturing analytics events. + * + * By default returns a noop tracker. When a `BUIProvider` is present + * in the tree, returns the tracker provided by the consumer's hook. + * + * @public + */ +export function useAnalytics(): AnalyticsTracker { + const ctx = useVersionedContext<{ 1: BUIContextValue }>('bui')?.atVersion(1); + const impl = ctx?.useAnalytics ?? noopUseAnalytics; + + if (process.env.NODE_ENV !== 'production') { + const prevImpl = useRef(impl); + if ( + (prevImpl.current === noopUseAnalytics) !== + (impl === noopUseAnalytics) + ) { + throw new Error( + '@backstage/ui: The analytics hook changed between a noop and a real ' + + 'implementation. Ensure wraps all BUI components from first render.', + ); + } + prevImpl.current = impl; + } + + return impl(); +} diff --git a/packages/ui/src/components/Accordion/Accordion.module.css b/packages/ui/src/components/Accordion/Accordion.module.css index cef40a6744..83ae6c24b8 100644 --- a/packages/ui/src/components/Accordion/Accordion.module.css +++ b/packages/ui/src/components/Accordion/Accordion.module.css @@ -19,21 +19,20 @@ @layer components { .bui-Accordion { width: 100%; - background-color: var(--bui-bg-neutral-1); border-radius: var(--bui-radius-3); padding: var(--bui-space-3); - &[data-on-bg='neutral-1'] { + &[data-bg='neutral-1'] { + background-color: var(--bui-bg-neutral-1); + } + + &[data-bg='neutral-2'] { background-color: var(--bui-bg-neutral-2); } - &[data-on-bg='neutral-2'] { + &[data-bg='neutral-3'] { background-color: var(--bui-bg-neutral-3); } - - &[data-on-bg='neutral-3'] { - background-color: var(--bui-bg-neutral-4); - } } .bui-AccordionTrigger { @@ -54,7 +53,7 @@ cursor: pointer; text-align: left; - &:focus-visible { + &[data-focus-visible] { outline: none; transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx index f87c36e5bc..5def4f57d3 100644 --- a/packages/ui/src/components/Accordion/Accordion.stories.tsx +++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx @@ -21,6 +21,7 @@ import { AccordionGroup, } from './Accordion'; import { Box } from '../Box'; +import { Button } from '../Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; @@ -187,41 +188,63 @@ export const AutoBg = meta.story({ + + + + - + Neutral 1 container + + + + - - Neutral 2 container - - - - - - - - + + + Neutral 2 container + + + + + + + + + + + + + - - Neutral 3 container - - - - - - - - + + + + Neutral 3 container + + + + + + + + + + + + + + ), diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx index 101a2d16c0..1a1eb2f2c8 100644 --- a/packages/ui/src/components/Accordion/Accordion.tsx +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -45,7 +45,7 @@ export const Accordion = forwardRef( AccordionDefinition, props, ); - const { classes } = ownProps; + const { classes, childrenWithBgProvider } = ownProps; return ( + > + {childrenWithBgProvider} + ); }, ); diff --git a/packages/ui/src/components/Accordion/definition.ts b/packages/ui/src/components/Accordion/definition.ts index 4c525e3cc6..2e76f0cd77 100644 --- a/packages/ui/src/components/Accordion/definition.ts +++ b/packages/ui/src/components/Accordion/definition.ts @@ -32,8 +32,10 @@ export const AccordionDefinition = defineComponent()({ classNames: { root: 'bui-Accordion', }, - bg: 'consumer', + bg: 'provider', propDefs: { + bg: { dataAttribute: true, default: 'neutral' }, + children: {}, className: {}, }, }); diff --git a/packages/ui/src/components/Accordion/types.ts b/packages/ui/src/components/Accordion/types.ts index 9afc2f9a3f..39ce1ab5be 100644 --- a/packages/ui/src/components/Accordion/types.ts +++ b/packages/ui/src/components/Accordion/types.ts @@ -21,12 +21,15 @@ import type { DisclosurePanelProps as RADisclosurePanelProps, DisclosureGroupProps as RADisclosureGroupProps, } from 'react-aria-components'; +import type { ProviderBg } from '../../types'; /** * Own props for the Accordion component. * @public */ export type AccordionOwnProps = { + bg?: ProviderBg; + children: ReactNode; className?: string; }; @@ -35,7 +38,7 @@ export type AccordionOwnProps = { * @public */ export interface AccordionProps - extends Omit, + extends Omit, AccordionOwnProps {} /** diff --git a/packages/ui/src/components/Alert/Alert.stories.tsx b/packages/ui/src/components/Alert/Alert.stories.tsx index 28239becb9..589d55b7dd 100644 --- a/packages/ui/src/components/Alert/Alert.stories.tsx +++ b/packages/ui/src/components/Alert/Alert.stories.tsx @@ -297,7 +297,7 @@ export const OnDifferentBackgrounds = meta.story({ On Neutral 1 - + @@ -305,18 +305,24 @@ export const OnDifferentBackgrounds = meta.story({ On Neutral 2 - - - - + + + + + + On Neutral 3 - - - - + + + + + + + + ), @@ -337,7 +343,7 @@ export const Responsive = meta.story({ export const WithUtilityProps = meta.story({ render: () => ( - + , + keyof AlertOwnProps | keyof MarginProps + > {} diff --git a/packages/ui/src/components/Avatar/Avatar.tsx b/packages/ui/src/components/Avatar/Avatar.tsx index 920a9b127f..e1694d738a 100644 --- a/packages/ui/src/components/Avatar/Avatar.tsx +++ b/packages/ui/src/components/Avatar/Avatar.tsx @@ -15,24 +15,18 @@ */ import { forwardRef, useState, useEffect } from 'react'; -import clsx from 'clsx'; import { AvatarProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { AvatarDefinition } from './definition'; -import styles from './Avatar.module.css'; /** @public */ export const Avatar = forwardRef((props, ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles( + const { ownProps, restProps, dataAttributes } = useDefinition( AvatarDefinition, - { - size: 'medium', - purpose: 'informative', - ...props, - }, + props, ); - const { className, src, name, purpose, ...rest } = cleanedProps; + const { classes, size, src, name, purpose } = ownProps; const [imageStatus, setImageStatus] = useState< 'loading' | 'loaded' | 'error' @@ -51,9 +45,7 @@ export const Avatar = forwardRef((props, ref) => { }; }, [src]); - const initialsCount = ['x-small', 'small'].includes(cleanedProps.size) - ? 1 - : 2; + const initialsCount = ['x-small', 'small'].includes(size) ? 1 : 2; const initials = name .split(' ') @@ -68,21 +60,14 @@ export const Avatar = forwardRef((props, ref) => { role="img" aria-label={purpose === 'informative' ? name : undefined} aria-hidden={purpose === 'decoration' ? true : undefined} - className={clsx(classNames.root, styles[classNames.root], className)} + className={classes.root} {...dataAttributes} - {...rest} + {...restProps} > {imageStatus === 'loaded' ? ( - + ) : ( - - + Neutral 1 container - - Neutral 2 container - - - - + + + Neutral 2 container + + + + + - - Neutral 3 container - - - - + + + + Neutral 3 container + + + + + + ), diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index d15ceabd77..21d3c7017b 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -39,6 +39,5 @@ export const ButtonDefinition = defineComponent()({ iconEnd: {}, children: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/Button/types.ts b/packages/ui/src/components/Button/types.ts index a18d9e9164..2bf36d808e 100644 --- a/packages/ui/src/components/Button/types.ts +++ b/packages/ui/src/components/Button/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReactElement, ReactNode, CSSProperties } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import type { ButtonProps as RAButtonProps } from 'react-aria-components'; import type { Responsive } from '../../types'; @@ -28,7 +28,6 @@ export type ButtonOwnProps = { loading?: boolean; children?: ReactNode; className?: string; - style?: CSSProperties; }; /** @@ -37,5 +36,5 @@ export type ButtonOwnProps = { * @public */ export interface ButtonProps - extends Omit, + extends Omit, ButtonOwnProps {} diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css index 128e958594..657b143ed5 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css @@ -73,7 +73,7 @@ --fg: var(--bui-fg-solid-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: 2px solid var(--bui-ring); outline-offset: 2px; } @@ -110,7 +110,7 @@ --fg: var(--bui-fg-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: none; transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); @@ -144,7 +144,7 @@ --fg: var(--bui-fg-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: none; transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 76f6be68ce..0027227094 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -36,6 +36,5 @@ export const ButtonIconDefinition = defineComponent()({ loading: { dataAttribute: true }, icon: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/ButtonIcon/types.ts b/packages/ui/src/components/ButtonIcon/types.ts index 92618d5ff6..f30f01ff99 100644 --- a/packages/ui/src/components/ButtonIcon/types.ts +++ b/packages/ui/src/components/ButtonIcon/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReactElement, CSSProperties } from 'react'; +import type { ReactElement } from 'react'; import type { ButtonProps as RAButtonProps } from 'react-aria-components'; import type { Responsive } from '../../types'; @@ -25,7 +25,6 @@ export type ButtonIconOwnProps = { icon?: ReactElement; loading?: boolean; className?: string; - style?: CSSProperties; }; /** @@ -34,5 +33,5 @@ export type ButtonIconOwnProps = { * @public */ export interface ButtonIconProps - extends Omit, + extends Omit, ButtonIconOwnProps {} diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.module.css b/packages/ui/src/components/ButtonLink/ButtonLink.module.css index 74a3fe6ba6..3c221ab878 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.module.css +++ b/packages/ui/src/components/ButtonLink/ButtonLink.module.css @@ -67,7 +67,7 @@ --fg: var(--bui-fg-solid-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: 2px solid var(--bui-ring); outline-offset: 2px; } @@ -103,7 +103,7 @@ --fg: var(--bui-fg-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: none; transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); @@ -136,7 +136,7 @@ --fg: var(--bui-fg-disabled); } - &:focus-visible { + &[data-focus-visible] { outline: none; transition: none; box-shadow: inset 0 0 0 2px var(--bui-ring); diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx index 4b9b9b747c..5be8a7edca 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx @@ -19,6 +19,7 @@ import type { StoryFn } from '@storybook/react-vite'; import { ButtonLink } from './ButtonLink'; import { Flex } from '../Flex'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react'; const meta = preview.meta({ @@ -27,7 +28,9 @@ const meta = preview.meta({ decorators: [ (Story: StoryFn) => ( - + + + ), ], diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index ac691897c4..4d6906f32f 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -19,32 +19,42 @@ import { Link as RALink } from 'react-aria-components'; import type { ButtonLinkProps } from './types'; import { useDefinition } from '../../hooks/useDefinition'; import { ButtonLinkDefinition } from './definition'; -import { InternalLinkProvider } from '../InternalLinkProvider'; +import { getNodeText } from '../../analytics/getNodeText'; /** @public */ export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { - const { ownProps, restProps, dataAttributes } = useDefinition( + const { ownProps, restProps, dataAttributes, analytics } = useDefinition( ButtonLinkDefinition, props, ); const { classes, iconStart, iconEnd, children } = ownProps; + const handlePress: typeof restProps.onPress = e => { + restProps.onPress?.(e); + const text = + restProps['aria-label'] ?? + getNodeText(children) ?? + String(restProps.href ?? ''); + analytics.captureEvent('click', text, { + attributes: { to: String(restProps.href ?? '') }, + }); + }; + return ( - - - - {iconStart} - {children} - {iconEnd} - - - + + + {iconStart} + {children} + {iconEnd} + + ); }, ); diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index c86cb026b5..a5f6a28489 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -29,13 +29,14 @@ export const ButtonLinkDefinition = defineComponent()({ content: 'bui-ButtonLinkContent', }, bg: 'consumer', + analytics: true, propDefs: { + noTrack: {}, size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, iconStart: {}, iconEnd: {}, children: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/ButtonLink/types.ts b/packages/ui/src/components/ButtonLink/types.ts index 0f01c3cf40..a4c5dbe65e 100644 --- a/packages/ui/src/components/ButtonLink/types.ts +++ b/packages/ui/src/components/ButtonLink/types.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import type { ReactElement, ReactNode, CSSProperties } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import type { LinkProps as RALinkProps } from 'react-aria-components'; import type { Responsive } from '../../types'; /** @public */ export type ButtonLinkOwnProps = { + noTrack?: boolean; size?: Responsive<'small' | 'medium'>; variant?: Responsive<'primary' | 'secondary' | 'tertiary'>; iconStart?: ReactElement; iconEnd?: ReactElement; children?: ReactNode; className?: string; - style?: CSSProperties; }; /** @@ -35,5 +35,5 @@ export type ButtonLinkOwnProps = { * @public */ export interface ButtonLinkProps - extends Omit, + extends Omit, ButtonLinkOwnProps {} diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 445873f6ba..eced185550 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -20,13 +20,76 @@ .bui-Card { display: flex; flex-direction: column; - gap: var(--bui-space-3); border-radius: var(--bui-radius-3); - padding-block: var(--bui-space-3); color: var(--bui-fg-primary); - overflow: hidden; + overflow: auto; min-height: 0; width: 100%; + position: relative; + padding: var(--bui-space-3); + } + + .bui-Card[data-bg='neutral-1'] { + --bui-card-bg: var(--bui-bg-neutral-1); + } + + .bui-Card[data-bg='neutral-2'] { + --bui-card-bg: var(--bui-bg-neutral-2); + } + + .bui-Card[data-bg='neutral-3'] { + --bui-card-bg: var(--bui-bg-neutral-3); + } + + .bui-Card:has(.bui-CardHeader, .bui-CardBody, .bui-CardFooter) { + padding: 0; + } + + /* + * Cursor and hover tint are applied at the card level so they cover the + * entire surface. + */ + .bui-Card[data-interactive] { + cursor: pointer; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: color-mix(in srgb, currentColor 5%, transparent); + border-radius: inherit; + pointer-events: none; + z-index: 3; + opacity: 0; + transition: opacity 200ms ease-in-out; + } + + &:hover::after { + opacity: 1; + } + } + + .bui-Card[data-interactive]:has(.bui-CardTrigger[data-focus-visible]) { + outline: 2px solid var(--bui-ring); + outline-offset: -2px; + } + + .bui-CardTrigger { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(100%); + white-space: nowrap; + border: 0; + } + + .bui-CardHeader { + padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } .bui-CardBody { @@ -34,13 +97,67 @@ min-height: 0; overflow: auto; padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } - .bui-CardHeader { - padding-inline: var(--bui-space-3); + @keyframes bui-card-body-shadow { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + .bui-Card:has(.bui-CardHeader) .bui-CardBody { + padding-block-start: 0; + + &::before { + content: ''; + position: sticky; + top: 0; + display: block; + height: 1.5rem; + margin-bottom: -1.5rem; + background: linear-gradient( + to bottom, + var(--bui-card-bg), + rgb(from var(--bui-card-bg) r g b / 0) + ); + pointer-events: none; + opacity: 0; + animation: bui-card-body-shadow linear both; + animation-timeline: scroll(); + animation-range: 0px 2.5rem; + } + } + + .bui-Card:has(.bui-CardFooter) .bui-CardBody { + padding-block-end: 0; + + &::after { + content: ''; + position: sticky; + bottom: 0; + display: block; + height: 1.5rem; + margin-top: -1.5rem; + background: linear-gradient( + to top, + var(--bui-card-bg), + rgb(from var(--bui-card-bg) r g b / 0) + ); + pointer-events: none; + opacity: 0; + animation: bui-card-body-shadow linear forwards, + bui-card-body-shadow linear forwards reverse; + animation-timeline: scroll(), scroll(); + animation-range: 0px 1px, calc(100% - 2.5rem) 100%; + } } .bui-CardFooter { padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } } diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 3957b7beff..bb756d312b 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -27,49 +27,64 @@ const meta = preview.meta({ }); export const Default = meta.story({ + render: args => Hello world, +}); + +export const DefaultWithHeader = meta.story({ render: args => ( Header Body - Footer ), }); -export const CustomSize = Default.extend({ - args: { - style: { - width: '300px', - height: '200px', - }, - }, +const content = ( + <> + + This is the first paragraph of a long body text that demonstrates how the + Card component handles extensive content. The card should adjust + accordingly to display all the text properly while maintaining its + structure. + + + Here's a second paragraph that adds more content to our card body. Having + multiple paragraphs helps to visualize how spacing works within the card + component. + + + This third paragraph continues to add more text to ensure we have a proper + demonstration of a card with significant content. This makes it easier to + test scrolling behavior and overall layout when content exceeds the + initial view. + + +); + +export const LongBody = meta.story({ + render: () => ( + {content} + ), }); -export const WithLongBody = meta.story({ +export const LongBodyHeader = meta.story({ render: () => ( Header - - - This is the first paragraph of a long body text that demonstrates how - the Card component handles extensive content. The card should adjust - accordingly to display all the text properly while maintaining its - structure. - - - Here's a second paragraph that adds more content to our card body. - Having multiple paragraphs helps to visualize how spacing works within - the card component. - - - This third paragraph continues to add more text to ensure we have a - proper demonstration of a card with significant content. This makes it - easier to test scrolling behavior and overall layout when content - exceeds the initial view. - - + {content} + + ), +}); + +export const LongBodyHeaderFooter = meta.story({ + render: () => ( + + + Header + + {content} Footer @@ -77,7 +92,7 @@ export const WithLongBody = meta.story({ ), }); -const ListRow = ({ children }: { children: React.ReactNode }) => { +const ListRowComponent = ({ children }: { children: React.ReactNode }) => { return (
    { paddingInline: 'var(--bui-space-3)', borderRadius: 'var(--bui-radius-2)', fontSize: 'var(--bui-font-size-3)', - marginBottom: 'var(--bui-space-1)', }} > {children} @@ -97,29 +111,76 @@ const ListRow = ({ children }: { children: React.ReactNode }) => { ); }; -export const WithListRow = meta.story({ +const listRowContent = ( + + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + +); + +export const ListRow = meta.story({ + render: () => ( + + {listRowContent} + + ), +}); + +export const ListRowHeader = meta.story({ + render: () => ( + + + Header + + {listRowContent} + + ), +}); + +export const ListRowFooter = meta.story({ + render: () => ( + + {listRowContent} + + Footer + + + ), +}); + +export const ListRowHeaderFooter = meta.story({ render: () => ( Header - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world + + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Footer @@ -135,23 +196,29 @@ export const Backgrounds = meta.story({ No parent Defaults to neutral-1 - + On neutral-1 Auto-increments to neutral-2 - - - On neutral-2 - Auto-increments to neutral-3 - + + + + On neutral-2 + Auto-increments to neutral-3 + + - - - On neutral-3 - Steps up to neutral-4 - + + + + + On neutral-3 + Steps up to neutral-4 + + + ), @@ -197,28 +264,139 @@ export const BgOnProviders = meta.story({ No provider Card defaults to neutral-1 - + On neutral-1 Card auto-increments to neutral-2 - - - On neutral-2 - Card auto-increments to neutral-3 - + + + + On neutral-2 + Card auto-increments to neutral-3 + + - - - On neutral-3 - Card visually at neutral-4 - + + + + + On neutral-3 + Card visually at neutral-4 + + + ), }); +export const Interactive = meta.story({ + render: () => ( + alert('Card pressed')} + label="View component details" + > + + Interactive Card + + + + Click anywhere on this card to trigger the press handler. The entire + card surface is interactive. + + + + + Click to interact + + + + ), +}); + +export const InteractiveAsLink = meta.story({ + render: () => ( + + Link Card + + + This card navigates to a URL when clicked. The entire card surface + acts as a link. + + + Opens backstage.io + + ), +}); + +export const InteractiveWithNestedButtons = meta.story({ + render: () => ( + alert('Card pressed')} + label="View plugin details" + > + + Card with Actions + + + + Clicking the card background triggers the card press handler. The + buttons below remain independently interactive. + + + + + + + + + + ), +}); + +export const InteractiveScrollable = meta.story({ + render: () => ( + alert('Card pressed')} + label="View card details" + > + + Scrollable Interactive Card + + {content} + + + Card body scrolls while card remains clickable + + + + ), +}); + export const CustomCardWithBox = meta.story({ render: () => ( @@ -226,14 +404,8 @@ export const CustomCardWithBox = meta.story({ A custom card built with Box. Use Box with an explicit bg prop to create a card-like container that participates in the bg system as a provider. - - + + Header diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index e3e4781eac..aecca09512 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { forwardRef } from 'react'; +import { forwardRef, useCallback, useRef } from 'react'; +import { Button as RAButton } from 'react-aria-components'; import { useDefinition } from '../../hooks/useDefinition'; import { CardDefinition, @@ -29,6 +30,10 @@ import type { CardFooterProps, } from './types'; import { Box } from '../Box/Box'; +import { Link } from '../Link'; + +const INTERACTIVE_ELEMENT_SELECTOR = + 'a[href],button,input,select,textarea,[role="button"],[role="link"],[tabindex]:not([tabindex="-1"])'; /** * Card component. @@ -40,16 +45,79 @@ export const Card = forwardRef((props, ref) => { CardDefinition, props, ); - const { classes, children } = ownProps; + const { + classes, + children, + onPress, + href, + label, + target: linkTarget, + rel, + download, + } = ownProps; + const isInteractive = !!(onPress || href); + + const triggerRef = useRef(null); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!isInteractive || !triggerRef.current) return; + + // Don't delegate if the click target is the trigger itself + if (triggerRef.current.contains(e.target as Node)) return; + + // Don't delegate if the user clicked a nested interactive element + const targetNode = e.target as Node | null; + const targetElement = + targetNode instanceof Element ? targetNode : targetNode?.parentElement; + if (targetElement?.closest(INTERACTIVE_ELEMENT_SELECTOR)) return; + + // Don't delegate if the user is selecting text + if (window.getSelection()?.toString()) return; + + triggerRef.current.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + shiftKey: e.shiftKey, + }), + ); + }, + [isInteractive], + ); return ( + {href && ( + } + className={classes.trigger} + href={href} + target={linkTarget} + rel={rel} + download={download} + aria-label={label} + /> + )} + {onPress && !href && ( + } + className={classes.trigger} + onPress={onPress} + aria-label={label} + /> + )} {children} ); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index 6493d01839..5d69005b4c 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -31,10 +31,17 @@ export const CardDefinition = defineComponent()({ styles, classNames: { root: 'bui-Card', + trigger: 'bui-CardTrigger', }, propDefs: { children: {}, className: {}, + onPress: {}, + href: {}, + label: {}, + target: {}, + rel: {}, + download: {}, }, }); diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index e31b2cfda2..5809384c16 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -15,11 +15,46 @@ */ import type { ReactNode } from 'react'; +import type { ButtonProps as RAButtonProps } from 'react-aria-components'; /** @public */ -export type CardOwnProps = { - children?: ReactNode; - className?: string; +export type CardBaseProps = { children?: ReactNode; className?: string }; + +/** @public */ +export type CardButtonVariant = { + /** Handler called when the card is pressed. Makes the card interactive as a button. */ + onPress: NonNullable; + href?: never; + /** Accessible label announced by screen readers for the interactive card. */ + label: string; + target?: never; + rel?: never; + download?: never; +}; + +/** @public */ +export type CardLinkVariant = { + /** URL to navigate to. Makes the card interactive as a link. */ + href: string; + onPress?: never; + /** Accessible label announced by screen readers for the interactive card. */ + label: string; + /** Specifies where to open the linked URL (e.g. `_blank` for a new tab). */ + target?: string; + /** Relationship between the current document and the linked URL (e.g. `noopener`). */ + rel?: string; + /** Prompts the user to save the linked URL. Pass `true` for default filename or a string for a custom filename. */ + download?: boolean | string; +}; + +/** @public */ +export type CardStaticVariant = { + onPress?: never; + href?: never; + label?: never; + target?: never; + rel?: never; + download?: never; }; /** @@ -27,9 +62,26 @@ export type CardOwnProps = { * * @public */ -export interface CardProps - extends CardOwnProps, - React.HTMLAttributes {} +export type CardProps = CardBaseProps & + Omit, 'onClick'> & + (CardButtonVariant | CardLinkVariant | CardStaticVariant); + +/** + * Flat own-props shape used by the component definition system. + * Derived from the Card variant types so it automatically stays in sync with CardProps. + * @public + */ +export type CardOwnProps = Pick< + CardBaseProps & (CardButtonVariant | CardLinkVariant | CardStaticVariant), + | 'children' + | 'className' + | 'onPress' + | 'href' + | 'label' + | 'target' + | 'rel' + | 'download' +>; /** @public */ export type CardHeaderOwnProps = { diff --git a/packages/ui/src/components/Checkbox/Checkbox.module.css b/packages/ui/src/components/Checkbox/Checkbox.module.css index deadc96358..54705d95e9 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.module.css +++ b/packages/ui/src/components/Checkbox/Checkbox.module.css @@ -20,7 +20,7 @@ .bui-Checkbox { display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; gap: var(--bui-space-2); font-size: var(--bui-font-size-3); font-family: var(--bui-font-regular); diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx index e8f7859785..94811d5cb6 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx @@ -16,6 +16,8 @@ import preview from '../../../../../.storybook/preview'; import { Checkbox } from './Checkbox'; import { Flex } from '../Flex'; +import { Link } from '../Link'; +import { MemoryRouter } from 'react-router-dom'; const meta = preview.meta({ title: 'Backstage UI/Checkbox', @@ -28,6 +30,12 @@ export const Default = meta.story({ }, }); +export const Selected = Default.extend({ + args: { + isSelected: true, + }, +}); + export const Indeterminate = meta.story({ args: { children: 'Select all', @@ -35,6 +43,25 @@ export const Indeterminate = meta.story({ }, }); +export const WithLongText = Default.extend({ + args: { + children: ( + <> + I agree to receive future communication from Spotify. You may + unsubscribe from these communications at any time. Please review our{' '} + Privacy Policy + + ), + }, + decorators: [ + Story => ( + + + + ), + ], +}); + export const AllVariants = meta.story({ ...Default.input, render: () => ( diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index 6030fbc06b..d7b0bd6f76 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -17,39 +17,36 @@ import { forwardRef } from 'react'; import { Checkbox as RACheckbox } from 'react-aria-components'; import type { CheckboxProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { CheckboxDefinition } from './definition'; -import clsx from 'clsx'; -import styles from './Checkbox.module.css'; import { RiCheckLine, RiSubtractLine } from '@remixicon/react'; /** @public */ export const Checkbox = forwardRef( (props, ref) => { - const { classNames } = useStyles(CheckboxDefinition); - const { className, children, ...rest } = props; + const { ownProps, restProps, dataAttributes } = useDefinition( + CheckboxDefinition, + props, + ); + const { classes, children } = ownProps; return ( {({ isIndeterminate }) => ( <> -
    +
    {isIndeterminate ? ( ) : ( )}
    - {children} +
    {children}
    )} diff --git a/packages/ui/src/components/Checkbox/definition.ts b/packages/ui/src/components/Checkbox/definition.ts index c7b5b16276..b6666924ee 100644 --- a/packages/ui/src/components/Checkbox/definition.ts +++ b/packages/ui/src/components/Checkbox/definition.ts @@ -14,19 +14,22 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { CheckboxOwnProps } from './types'; +import styles from './Checkbox.module.css'; /** * Component definition for Checkbox * @public */ -export const CheckboxDefinition = { +export const CheckboxDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Checkbox', indicator: 'bui-CheckboxIndicator', }, - dataAttributes: { - selected: [true, false] as const, - indeterminate: [true, false] as const, + propDefs: { + children: {}, + className: {}, }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/Checkbox/index.ts b/packages/ui/src/components/Checkbox/index.ts index 7c0cb5b414..bd20736c41 100644 --- a/packages/ui/src/components/Checkbox/index.ts +++ b/packages/ui/src/components/Checkbox/index.ts @@ -15,4 +15,4 @@ */ export { Checkbox } from './Checkbox'; export { CheckboxDefinition } from './definition'; -export type { CheckboxProps } from './types'; +export type { CheckboxOwnProps, CheckboxProps } from './types'; diff --git a/packages/ui/src/components/Checkbox/types.ts b/packages/ui/src/components/Checkbox/types.ts index 86e4b620b8..d9b220eb2a 100644 --- a/packages/ui/src/components/Checkbox/types.ts +++ b/packages/ui/src/components/Checkbox/types.ts @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CheckboxProps as RACheckboxProps } from 'react-aria-components'; +import type { CheckboxProps as RACheckboxProps } from 'react-aria-components'; /** @public */ -export interface CheckboxProps extends RACheckboxProps { +export type CheckboxOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +/** @public */ +export interface CheckboxProps + extends Omit, + CheckboxOwnProps {} diff --git a/packages/ui/src/components/Container/Container.module.css b/packages/ui/src/components/Container/Container.module.css index a0dec1e581..1ebf52ccf3 100644 --- a/packages/ui/src/components/Container/Container.module.css +++ b/packages/ui/src/components/Container/Container.module.css @@ -21,7 +21,7 @@ max-width: 120rem; padding-inline: var(--bui-space-4); margin-inline: auto; - transition: padding 0.2s ease-in-out; + margin-bottom: var(--bui-space-8); } @media (min-width: 640px) { diff --git a/packages/ui/src/components/Container/Container.stories.tsx b/packages/ui/src/components/Container/Container.stories.tsx index 44ff6f2d17..e6fbee091b 100644 --- a/packages/ui/src/components/Container/Container.stories.tsx +++ b/packages/ui/src/components/Container/Container.stories.tsx @@ -43,6 +43,7 @@ const DecorativeBox = () => ( backgroundImage: 'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")', }} + children={null} /> ); diff --git a/packages/ui/src/components/Container/Container.tsx b/packages/ui/src/components/Container/Container.tsx index 7e3627f0b9..743704b00e 100644 --- a/packages/ui/src/components/Container/Container.tsx +++ b/packages/ui/src/components/Container/Container.tsx @@ -15,34 +15,30 @@ */ import { forwardRef } from 'react'; -import { ContainerProps } from './types'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; +import type { ContainerProps } from './types'; +import { useDefinition } from '../../hooks/useDefinition'; import { ContainerDefinition } from './definition'; -import styles from './Container.module.css'; /** @public */ export const Container = forwardRef( (props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( + const { ownProps, restProps, utilityStyle } = useDefinition( ContainerDefinition, props, ); - - const { className, ...rest } = cleanedProps; + const { classes, children, style } = ownProps; return (
    + className={classes.root} + style={{ ...utilityStyle, ...style }} + {...restProps} + > + {children} +
    ); }, ); + +Container.displayName = 'Container'; diff --git a/packages/ui/src/components/Container/definition.ts b/packages/ui/src/components/Container/definition.ts index 330e6adf2f..e9d7ce3238 100644 --- a/packages/ui/src/components/Container/definition.ts +++ b/packages/ui/src/components/Container/definition.ts @@ -14,15 +14,23 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { ContainerOwnProps } from './types'; +import styles from './Container.module.css'; /** * Component definition for Container * @public */ -export const ContainerDefinition = { +export const ContainerDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Container', }, + propDefs: { + children: {}, + className: {}, + style: {}, + }, utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display'], -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/Container/index.tsx b/packages/ui/src/components/Container/index.tsx index 73d1a0ab6e..d7e8eed01e 100644 --- a/packages/ui/src/components/Container/index.tsx +++ b/packages/ui/src/components/Container/index.tsx @@ -15,4 +15,4 @@ */ export { Container } from './Container'; export { ContainerDefinition } from './definition'; -export type { ContainerProps } from './types'; +export type { ContainerOwnProps, ContainerProps } from './types'; diff --git a/packages/ui/src/components/Container/types.ts b/packages/ui/src/components/Container/types.ts index 41de725b32..375f3a4a38 100644 --- a/packages/ui/src/components/Container/types.ts +++ b/packages/ui/src/components/Container/types.ts @@ -13,17 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SpaceProps } from '../../types'; +import type { SpaceProps } from '../../types'; /** @public */ -export interface ContainerProps { +export type ContainerOwnProps = { children?: React.ReactNode; className?: string; + style?: React.CSSProperties; +}; + +/** @public */ +export interface ContainerProps + extends ContainerOwnProps, + Omit, keyof ContainerOwnProps> { my?: SpaceProps['my']; mb?: SpaceProps['mb']; mt?: SpaceProps['mt']; py?: SpaceProps['py']; pb?: SpaceProps['pb']; pt?: SpaceProps['pt']; - style?: React.CSSProperties; } diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index f5968820a2..194128ec4d 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -43,20 +43,31 @@ } .bui-Dialog { - background: var(--bui-bg-popover); - border-radius: 0.5rem; + --dialog-border-radius: 0.5rem; + background: var(--bui-bg-app); + box-shadow: var(--bui-shadow); + border-radius: var(--dialog-border-radius); border: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); position: relative; - width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem)); - max-width: calc(100vw - 3rem); - height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem)); - max-height: calc(100vh - 3rem); display: flex; flex-direction: column; + width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem)); + max-width: calc(100vw - 3rem); + height: var(--bui-dialog-height, auto); + max-height: calc(100vh - 3rem); outline: none; } + .bui-DialogContent { + display: flex; + flex-direction: column; + border-radius: var(--dialog-border-radius); + flex: 1; + min-height: 0; + overflow: hidden; + } + /* Dialog entering animation */ .bui-DialogOverlay[data-entering] .bui-Dialog { animation: dialog-enter 150ms ease-out forwards; @@ -95,6 +106,7 @@ .bui-DialogBody { padding: var(--bui-space-3); flex: 1; + min-height: 0; overflow-y: auto; } diff --git a/packages/ui/src/components/Dialog/Dialog.stories.tsx b/packages/ui/src/components/Dialog/Dialog.stories.tsx index 0392959a59..8892cb2314 100644 --- a/packages/ui/src/components/Dialog/Dialog.stories.tsx +++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx @@ -63,6 +63,24 @@ export const Default = meta.story({ }); export const Open = Default.extend({ + parameters: { layout: 'fullscreen' }, + decorators: [ + Story => ( +
    + +
    + ), + ], args: { defaultOpen: true, }, @@ -229,6 +247,39 @@ export const WithForm = meta.story({ ), }); +export const OverflowWithoutHeight = meta.story({ + args: { + defaultOpen: true, + }, + render: args => ( + + + + Overflow Without Height + + + {Array.from({ length: 20 }, (_, i) => ( + + Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + ))} + + + + + + + + + ), +}); + export const PreviewFixedWidthAndHeight = FixedWidth.extend({ args: { defaultOpen: undefined, diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx index 1c33588306..3a23ee27c9 100644 --- a/packages/ui/src/components/Dialog/Dialog.tsx +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -21,19 +21,25 @@ import { Modal, Heading, } from 'react-aria-components'; -import clsx from 'clsx'; import type { DialogTriggerProps, DialogHeaderProps, DialogProps, DialogBodyProps, + DialogFooterProps, } from './types'; import { RiCloseLine } from '@remixicon/react'; import { Button } from '../Button'; -import { useStyles } from '../../hooks/useStyles'; -import { DialogDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + DialogDefinition, + DialogHeaderDefinition, + DialogBodyDefinition, + DialogFooterDefinition, +} from './definition'; +import { Box } from '../Box'; +import { BgReset } from '../../hooks/useBg'; import { Flex } from '../Flex'; -import styles from './Dialog.module.css'; /** @public */ export const DialogTrigger = (props: DialogTriggerProps) => { @@ -43,35 +49,38 @@ export const DialogTrigger = (props: DialogTriggerProps) => { /** @public */ export const Dialog = forwardRef, DialogProps>( (props, ref) => { - const { classNames, cleanedProps } = useStyles(DialogDefinition, props); - const { className, children, width, height, style, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(DialogDefinition, props, { + classNameTarget: 'dialog', + }); + const { classes, children, width, height, style } = ownProps; return ( - {children} + + + {children} + + ); @@ -85,19 +94,12 @@ export const DialogHeader = forwardRef< React.ElementRef<'div'>, DialogHeaderProps >((props, ref) => { - const { classNames, cleanedProps } = useStyles(DialogDefinition, props); - const { className, children, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(DialogHeaderDefinition, props); + const { classes, children } = ownProps; return ( - - + + {children} } @@ -239,32 +242,34 @@ export const WithTabsMatchingStrategies = meta.story({ }, render: args => ( - - - - Current URL: /mentorship/events - -
    - - Notice how the "Mentorship" tab is active even though we're on a - nested route. This is because it uses{' '} - matchStrategy="prefix". - -
    - - • Home: exact matching (default) - not active - - - • Mentorship: prefix matching - IS active (URL starts - with /mentorship) - - - • Catalog: prefix matching - not active - - - • Settings: exact matching (default) - not active - -
    + +
    + + + Current URL: /mentorship/events + +
    + + Notice how the "Mentorship" tab is active even though we're on a + nested route. This is because it uses{' '} + matchStrategy="prefix". + +
    + + • Home: exact matching (default) - not active + + + • Mentorship: prefix matching - IS active (URL + starts with /mentorship) + + + • Catalog: prefix matching - not active + + + • Settings: exact matching (default) - not active + +
    + ), }); @@ -292,18 +297,20 @@ export const WithTabsExactMatching = meta.story({ }, render: args => ( - - - - Current URL: /mentorship/events - -
    - - With default exact matching, only the "Events" tab is active because - it exactly matches the current URL. The "Mentorship" tab is not active - even though the URL is under /mentorship. - -
    + +
    + + + Current URL: /mentorship/events + +
    + + With default exact matching, only the "Events" tab is active because + it exactly matches the current URL. The "Mentorship" tab is not + active even though the URL is under /mentorship. + +
    + ), }); @@ -334,33 +341,36 @@ export const WithTabsPrefixMatchingDeep = meta.story({ }, render: args => ( - - - - Current URL: /catalog/users/john/details - -
    - - Active tab is Users because: - -
      -
    • - Catalog: Matches since URL starts with /catalog -
    • -
    • - Users: Is active since URL starts with - /catalog/users, and is more specific (has more url segments) than - "Catalog" -
    • -
    • - Components: not active (URL doesn't start with - /catalog/components) -
    • -
    - - This demonstrates how prefix matching works with deeply nested routes. - -
    + +
    + + + Current URL: /catalog/users/john/details + +
    + + Active tab is Users because: + +
      +
    • + Catalog: Matches since URL starts with /catalog +
    • +
    • + Users: Is active since URL starts with + /catalog/users, and is more specific (has more url segments) than + "Catalog" +
    • +
    • + Components: not active (URL doesn't start with + /catalog/components) +
    • +
    + + This demonstrates how prefix matching works with deeply nested + routes. + +
    + ), }); diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.tsx b/packages/ui/src/components/Header/Header.tsx similarity index 65% rename from packages/ui/src/components/HeaderPage/HeaderPage.tsx rename to packages/ui/src/components/Header/Header.tsx index 8b1ef31874..d9d96eb7af 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -14,38 +14,29 @@ * limitations under the License. */ -import type { HeaderPageProps } from './types'; +import type { HeaderProps } from './types'; import { Text } from '../Text'; import { RiArrowRightSLine } from '@remixicon/react'; import { Tabs, TabList, Tab } from '../Tabs'; -import { useStyles } from '../../hooks/useStyles'; -import { HeaderPageDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { HeaderDefinition } from './definition'; import { Container } from '../Container'; import { Link } from '../Link'; import { Fragment } from 'react/jsx-runtime'; -import styles from './HeaderPage.module.css'; -import clsx from 'clsx'; /** - * A component that renders a header page. + * A secondary header with title, breadcrumbs, tabs, and actions. * * @public */ -export const HeaderPage = (props: HeaderPageProps) => { - const { classNames, cleanedProps } = useStyles(HeaderPageDefinition, props); - const { className, title, tabs, customActions, breadcrumbs } = cleanedProps; +export const Header = (props: HeaderProps) => { + const { ownProps } = useDefinition(HeaderDefinition, props); + const { classes, title, tabs, customActions, breadcrumbs } = ownProps; return ( - -
    -
    + +
    +
    {breadcrumbs && breadcrumbs.map(breadcrumb => ( @@ -67,17 +58,10 @@ export const HeaderPage = (props: HeaderPageProps) => { {title}
    -
    - {customActions} -
    +
    {customActions}
    {tabs && ( -
    +
    {tabs.map(tab => ( @@ -97,3 +81,9 @@ export const HeaderPage = (props: HeaderPageProps) => { ); }; + +/** + * @public + * @deprecated Use {@link Header} instead. + */ +export const HeaderPage = Header; diff --git a/packages/ui/src/components/Header/definition.ts b/packages/ui/src/components/Header/definition.ts new file mode 100644 index 0000000000..66fccf2f49 --- /dev/null +++ b/packages/ui/src/components/Header/definition.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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 { defineComponent } from '../../hooks/useDefinition'; +import type { HeaderOwnProps } from './types'; +import styles from './Header.module.css'; + +/** + * Component definition for Header + * @public + */ +export const HeaderDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-Header', + content: 'bui-HeaderContent', + breadcrumbs: 'bui-HeaderBreadcrumbs', + tabsWrapper: 'bui-HeaderTabsWrapper', + controls: 'bui-HeaderControls', + }, + propDefs: { + title: {}, + customActions: {}, + tabs: {}, + breadcrumbs: {}, + className: {}, + }, +}); + +/** + * @public + * @deprecated Use {@link HeaderDefinition} instead. + */ +export const HeaderPageDefinition = HeaderDefinition; diff --git a/packages/ui/src/components/Header/index.tsx b/packages/ui/src/components/Header/index.tsx new file mode 100644 index 0000000000..8738114e7d --- /dev/null +++ b/packages/ui/src/components/Header/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { Header, HeaderPage } from './Header'; +export { HeaderDefinition, HeaderPageDefinition } from './definition'; +export type { + HeaderOwnProps, + HeaderProps, + HeaderBreadcrumb, + HeaderPageOwnProps, + HeaderPageProps, + HeaderPageBreadcrumb, +} from './types'; diff --git a/packages/ui/src/components/HeaderPage/types.ts b/packages/ui/src/components/Header/types.ts similarity index 59% rename from packages/ui/src/components/HeaderPage/types.ts rename to packages/ui/src/components/Header/types.ts index 72d365968e..1b62ee2538 100644 --- a/packages/ui/src/components/HeaderPage/types.ts +++ b/packages/ui/src/components/Header/types.ts @@ -17,24 +17,49 @@ import type { HeaderTab } from '../PluginHeader/types'; /** - * Props for the main HeaderPage component. + * Own props for the Header component. * * @public */ -export interface HeaderPageProps { +export interface HeaderOwnProps { title?: string; customActions?: React.ReactNode; tabs?: HeaderTab[]; - breadcrumbs?: HeaderPageBreadcrumb[]; + breadcrumbs?: HeaderBreadcrumb[]; className?: string; } +/** + * Props for the Header component. + * + * @public + */ +export interface HeaderProps extends HeaderOwnProps {} + /** * Represents a breadcrumb item in the header. * * @public */ -export interface HeaderPageBreadcrumb { +export interface HeaderBreadcrumb { label: string; href: string; } + +/** + * @public + * @deprecated Use {@link HeaderOwnProps} instead. + */ +export type HeaderPageOwnProps = HeaderOwnProps; + +/** + * @public + * @deprecated Use {@link HeaderProps} instead. + */ +export type HeaderPageProps = HeaderProps; + +/** + * @public + * @deprecated Use {@link HeaderBreadcrumb} instead. + */ +export type HeaderPageBreadcrumb = HeaderBreadcrumb; diff --git a/packages/ui/src/components/InternalLinkProvider/InternalLinkProvider.tsx b/packages/ui/src/components/InternalLinkProvider/InternalLinkProvider.tsx deleted file mode 100644 index 3c8a2ac456..0000000000 --- a/packages/ui/src/components/InternalLinkProvider/InternalLinkProvider.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 { - ReactNode, - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import { RouterProvider } from 'react-aria-components'; -import { useNavigate, useHref } from 'react-router-dom'; -import { isExternalLink } from '../../utils/isExternalLink'; - -/** - * Checks if an href is an internal link (not external and not empty). - * - * @internal - */ -export function isInternalLink(href: string | undefined): href is string { - return !!href && !isExternalLink(href); -} - -/** - * Context value type for routing registration. - * Used by container components to track children that need RouterProvider. - * - * @internal - */ -export type RoutingContextValue = { - register: () => () => void; -}; - -/** - * Wraps children in a RouterProvider for client-side navigation. - * Must be rendered within a React Router context. - * - * @internal - */ -export function RoutedContainer({ children }: { children: ReactNode }) { - const navigate = useNavigate(); - return ( - - {children} - - ); -} - -/** - * Hook for container components that need to conditionally provide routing. - * - * Usage: - * 1. Call this hook in the container component - * 2. Pass `contextValue` to a RoutingContextValue context provider - * 3. Children call `register()` via context when they have internal hrefs - * 4. If `hasRoutedChildren` is true, wrap content in RoutedContainer - * - * @internal - */ -export function useRoutingRegistration(): { - hasRoutedChildren: boolean; - contextValue: RoutingContextValue; -} { - const [count, setCount] = useState(0); - - const register = useCallback(() => { - setCount(c => c + 1); - return () => setCount(c => c - 1); - }, []); - - return { hasRoutedChildren: count > 0, contextValue: { register } }; -} - -/** - * Creates a routing registration context and provider for container components. - * - * Usage: - * ```tsx - * // At module level - * const { RoutingProvider, useRoutingRegistrationEffect } = createRoutingRegistration(); - * - * // Container component wraps content with provider - * {content} - * - * // Child items register when they have internal hrefs - * useRoutingRegistrationEffect(href); - * ``` - * - * @internal - */ -export function createRoutingRegistration() { - const RoutingContext = createContext(null); - - function RoutingProvider({ children }: { children: ReactNode }) { - const { hasRoutedChildren, contextValue } = useRoutingRegistration(); - - const content = ( - - {children} - - ); - - if (hasRoutedChildren) { - return {content}; - } - - return content; - } - - function useRoutingRegistrationEffect(href: string | undefined) { - const routingCtx = useContext(RoutingContext); - const hasInternalHref = isInternalLink(href); - - useEffect(() => { - if (hasInternalHref && routingCtx) { - return routingCtx.register(); - } - return undefined; - }, [hasInternalHref, routingCtx]); - } - - return { RoutingContext, RoutingProvider, useRoutingRegistrationEffect }; -} - -/** - * Conditionally wraps children in a RouterProvider for internal link navigation. - * Only mounts the router hooks when `href` is an internal link, avoiding the - * requirement for a Router context when rendering components without internal hrefs. - * - * @internal - */ -export function InternalLinkProvider({ - href, - children, -}: { - href: string | undefined; - children: ReactNode; -}) { - if (!isInternalLink(href)) { - return <>{children}; - } - return {children}; -} diff --git a/packages/ui/src/components/Link/Link.stories.tsx b/packages/ui/src/components/Link/Link.stories.tsx index fafb5948d9..cf52cdde23 100644 --- a/packages/ui/src/components/Link/Link.stories.tsx +++ b/packages/ui/src/components/Link/Link.stories.tsx @@ -20,6 +20,7 @@ import { Link } from './Link'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/Link', @@ -30,7 +31,9 @@ const meta = preview.meta({ decorators: [ (Story: StoryFn) => ( - + + + ), ], diff --git a/packages/ui/src/components/Link/Link.tsx b/packages/ui/src/components/Link/Link.tsx index efdca26750..a4509576c9 100644 --- a/packages/ui/src/components/Link/Link.tsx +++ b/packages/ui/src/components/Link/Link.tsx @@ -16,52 +16,33 @@ import { forwardRef, useRef } from 'react'; import { useLink } from 'react-aria'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; -import { LinkDefinition } from './definition'; import type { LinkProps } from './types'; -import { InternalLinkProvider } from '../InternalLinkProvider'; -import styles from './Link.module.css'; +import { useDefinition } from '../../hooks/useDefinition'; +import { LinkDefinition } from './definition'; +import { getNodeText } from '../../analytics/getNodeText'; const LinkInternal = forwardRef((props, ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles( + const { ownProps, restProps, dataAttributes, analytics } = useDefinition( LinkDefinition, - { - variant: 'body', - weight: 'regular', - color: 'primary', - ...props, - }, + props, ); - - const { - className, - href, - title, - children, - onPress, - variant, - weight, - color, - truncate, - standalone, - slot, - ...restProps - } = cleanedProps; + const { classes, title, children } = ownProps; const internalRef = useRef(null); const linkRef = (ref || internalRef) as React.RefObject; - // Use useLink hook to get link props - // For internal links, this will use the RouterProvider's navigate function - const { linkProps } = useLink( - { - href, - onPress, - ...restProps, - }, - linkRef, - ); + const { linkProps } = useLink(restProps, linkRef); + + const handleClick = (e: React.MouseEvent) => { + linkProps.onClick?.(e); + const text = + restProps['aria-label'] ?? + getNodeText(children) ?? + String(restProps.href ?? ''); + analytics.captureEvent('click', text, { + attributes: { to: String(restProps.href ?? '') }, + }); + }; return ( ((props, ref) => { {...dataAttributes} {...(restProps as React.AnchorHTMLAttributes)} ref={linkRef} - href={href} title={title} - className={clsx(classNames.root, styles[classNames.root], className)} + className={classes.root} + onClick={handleClick} > {children} @@ -82,11 +63,7 @@ LinkInternal.displayName = 'LinkInternal'; /** @public */ export const Link = forwardRef((props, ref) => { - return ( - - - - ); + return ; }); Link.displayName = 'Link'; diff --git a/packages/ui/src/components/Link/definition.ts b/packages/ui/src/components/Link/definition.ts index 58b780c812..15d0d35e12 100644 --- a/packages/ui/src/components/Link/definition.ts +++ b/packages/ui/src/components/Link/definition.ts @@ -14,28 +14,29 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { LinkOwnProps } from './types'; +import styles from './Link.module.css'; /** * Component definition for Link * @public */ -export const LinkDefinition = { +export const LinkDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Link', }, - dataAttributes: { - variant: ['subtitle', 'body', 'caption', 'label'] as const, - weight: ['regular', 'bold'] as const, - color: [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - 'info', - ] as const, - truncate: [true, false] as const, - standalone: [true, false] as const, + analytics: true, + propDefs: { + noTrack: {}, + variant: { dataAttribute: true, default: 'body-medium' }, + weight: { dataAttribute: true, default: 'regular' }, + color: { dataAttribute: true, default: 'primary' }, + truncate: { dataAttribute: true }, + standalone: { dataAttribute: true }, + title: {}, + children: {}, + className: {}, }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/Link/index.ts b/packages/ui/src/components/Link/index.ts index 7c17668029..f7d335bd52 100644 --- a/packages/ui/src/components/Link/index.ts +++ b/packages/ui/src/components/Link/index.ts @@ -16,4 +16,4 @@ export { Link } from './Link'; export { LinkDefinition } from './definition'; -export type { LinkProps } from './types'; +export type { LinkOwnProps, LinkProps } from './types'; diff --git a/packages/ui/src/components/Link/types.ts b/packages/ui/src/components/Link/types.ts index 9259a8a1d7..75b059a42b 100644 --- a/packages/ui/src/components/Link/types.ts +++ b/packages/ui/src/components/Link/types.ts @@ -25,7 +25,8 @@ import type { LinkProps as AriaLinkProps } from 'react-aria-components'; import type { ReactNode } from 'react'; /** @public */ -export interface LinkProps extends AriaLinkProps { +export type LinkOwnProps = { + noTrack?: boolean; variant?: TextVariants | Partial>; weight?: TextWeights | Partial>; color?: @@ -34,10 +35,12 @@ export interface LinkProps extends AriaLinkProps { | Partial>; truncate?: boolean; standalone?: boolean; - - // This is used to set the title attribute on the link title?: string; - - // This is used to set the children of the link children?: ReactNode; -} + className?: string; +}; + +/** @public */ +export interface LinkProps + extends Omit, + LinkOwnProps {} diff --git a/packages/ui/src/components/List/List.module.css b/packages/ui/src/components/List/List.module.css new file mode 100644 index 0000000000..9968f0b5bc --- /dev/null +++ b/packages/ui/src/components/List/List.module.css @@ -0,0 +1,195 @@ +/* + * 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. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-List { + box-sizing: border-box; + outline: none; + display: flex; + flex-direction: column; + + gap: var(--bui-space-3); + + &:has([data-selection-mode]) { + gap: 0; + } + + &[data-focus-visible] { + outline: none; + } + } + + .bui-ListRow { + box-sizing: border-box; + display: flex; + align-items: center; + gap: var(--bui-space-3); + border-radius: var(--bui-radius-2); + font-size: var(--bui-font-size-3); + font-family: var(--bui-font-regular); + color: var(--bui-fg-primary); + outline: none; + + &[data-disabled] { + cursor: not-allowed; + color: var(--bui-fg-disabled); + } + + &[data-focus-visible] { + outline: 2px solid var(--bui-ring); + } + + &[data-selection-mode] { + cursor: pointer; + padding-block: var(--bui-space-2); + padding-inline: var(--bui-space-2); + + &[data-selected] { + &:has(+ [data-selected]) { + border-end-start-radius: 0; + border-end-end-radius: 0; + } + + + [data-selected] { + border-start-start-radius: 0; + border-start-end-radius: 0; + } + } + + &[data-hovered], + &[data-focus-visible] { + background-color: var(--bui-bg-neutral-1-hover); + } + + &[data-pressed], + &[data-selected], + &[data-selected][data-hovered], + &[data-selected][data-focus-visible], + &[data-selected][data-pressed] { + background-color: var(--bui-bg-neutral-1-pressed); + } + + &[data-on-bg='neutral-1'] { + &[data-hovered], + &[data-focus-visible] { + background-color: var(--bui-bg-neutral-2-hover); + } + + &[data-pressed], + &[data-selected], + &[data-selected][data-hovered], + &[data-selected][data-focus-visible], + &[data-selected][data-pressed] { + background-color: var(--bui-bg-neutral-2-pressed); + } + } + + &[data-on-bg='neutral-2'] { + &[data-hovered], + &[data-focus-visible] { + background-color: var(--bui-bg-neutral-3-hover); + } + + &[data-pressed], + &[data-selected], + &[data-selected][data-hovered], + &[data-selected][data-focus-visible], + &[data-selected][data-pressed] { + background-color: var(--bui-bg-neutral-3-pressed); + } + } + + &[data-on-bg='neutral-3'], + &[data-on-bg='neutral-4'] { + &[data-hovered], + &[data-focus-visible] { + background-color: var(--bui-bg-neutral-4-hover); + } + + &[data-pressed], + &[data-selected], + &[data-selected][data-hovered], + &[data-selected][data-focus-visible], + &[data-selected][data-pressed] { + background-color: var(--bui-bg-neutral-4-pressed); + } + } + } + } + + .bui-ListRowCheck { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 1rem; + height: 1rem; + + & svg { + width: 1rem; + height: 1rem; + } + } + + .bui-ListRowIcon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 2rem; + height: 2rem; + color: var(--bui-fg-secondary); + border-radius: var(--bui-radius-2); + + & svg { + width: 1rem; + height: 1rem; + } + } + + .bui-ListRowLabel { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--bui-space-1); + min-width: 0; + overflow: hidden; + + & > * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .bui-ListRowDescription { + font-size: var(--bui-font-size-2); + color: var(--bui-fg-secondary); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .bui-ListRowActions { + display: flex; + align-items: center; + gap: var(--bui-space-1); + flex-shrink: 0; + margin-left: auto; + } +} diff --git a/packages/ui/src/components/List/List.stories.tsx b/packages/ui/src/components/List/List.stories.tsx new file mode 100644 index 0000000000..224315ddac --- /dev/null +++ b/packages/ui/src/components/List/List.stories.tsx @@ -0,0 +1,290 @@ +/* + * 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 preview from '../../../../../.storybook/preview'; +import { useState } from 'react'; +import { List, ListRow } from './List'; +import { MenuItem } from '../Menu'; +import { TagGroup, Tag } from '../TagGroup'; +import type { Selection } from 'react-aria-components'; +import { + RiJavascriptLine, + RiReactjsLine, + RiShipLine, + RiTerminalLine, + RiCodeLine, + RiDeleteBinLine, + RiEdit2Line, + RiShareBoxLine, +} from '@remixicon/react'; +import { MemoryRouter } from 'react-router-dom'; + +const meta = preview.meta({ + title: 'Backstage UI/List', + component: List, + args: { + style: { width: 320 }, + 'aria-label': 'List', + }, + decorators: [ + Story => ( + + + + ), + ], +}); + +const items = [ + { + id: 'react', + label: 'React', + description: 'A JavaScript library for building user interfaces', + icon: , + tags: ['frontend', 'ui'], + }, + { + id: 'typescript', + label: 'TypeScript', + description: 'Typed superset of JavaScript', + icon: , + tags: ['typed', 'js'], + }, + { + id: 'javascript', + label: 'JavaScript', + description: 'The language of the web', + icon: , + tags: ['web'], + }, + { + id: 'rust', + label: 'Rust', + description: 'Systems programming with memory safety', + icon: , + tags: ['systems', 'fast'], + }, + { + id: 'go', + label: 'Go', + description: 'Simple, fast, and reliable', + icon: , + tags: ['backend'], + }, +]; + +const menuItems = ( + <> + }>Edit + }>Share + } color="danger"> + Delete + + +); + +export const Default = meta.story({ + render: args => ( + + {item => {item.label}} + + ), +}); + +export const WithIcons = meta.story({ + render: args => ( + + {item => ( + + {item.label} + + )} + + ), +}); + +export const WithDescription = meta.story({ + args: { + style: { width: 340 }, + }, + render: args => ( + + {item => ( + + {item.label} + + )} + + ), +}); + +export const SelectionModeSingle = meta.story({ + render: args => { + const [selected, setSelected] = useState(new Set(['react'])); + + return ( + + {item => {item.label}} + + ); + }, +}); + +export const SelectionModeSingleWithIcons = meta.story({ + render: args => { + const [selected, setSelected] = useState(new Set(['react'])); + + return ( + + {item => ( + + {item.label} + + )} + + ); + }, +}); + +export const SelectionModeMultiple = meta.story({ + render: args => { + const [selected, setSelected] = useState( + new Set(['react', 'typescript']), + ); + + return ( + + {item => {item.label}} + + ); + }, +}); + +export const SelectionModeMultipleWithIcons = meta.story({ + render: args => { + const [selected, setSelected] = useState( + new Set(['react', 'typescript']), + ); + + return ( + + {item => ( + + {item.label} + + )} + + ); + }, +}); + +export const Disabled = meta.story({ + render: args => ( + + {item => {item.label}} + + ), +}); + +export const WithActionsMenu = meta.story({ + args: { + style: { width: 420 }, + }, + render: args => ( + + {item => ( + + {item.label} + + )} + + ), +}); + +export const WithActionsTags = meta.story({ + args: { + style: { width: 420 }, + }, + render: args => ( + + {item => ( + + {item.tags.map(tag => ( + {tag} + ))} + + } + > + {item.label} + + )} + + ), +}); + +export const WithActionsMenuAndTags = meta.story({ + args: { + style: { width: 420 }, + }, + render: args => ( + + {item => ( + + {item.tags.map(tag => ( + {tag} + ))} + + } + > + {item.label} + + )} + + ), +}); diff --git a/packages/ui/src/components/List/List.tsx b/packages/ui/src/components/List/List.tsx new file mode 100644 index 0000000000..a8aeaa6727 --- /dev/null +++ b/packages/ui/src/components/List/List.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + GridList as RAGridList, + GridListItem as RAGridListItem, + Text, +} from 'react-aria-components'; +import { RiCheckLine, RiMoreLine } from '@remixicon/react'; +import { useDefinition } from '../../hooks/useDefinition'; +import { ListDefinition, ListRowDefinition } from './definition'; +import type { ListProps, ListRowProps } from './types'; +import { Box } from '../Box/Box'; +import { ButtonIcon } from '../ButtonIcon'; +import { MenuTrigger, Menu } from '../Menu'; + +/** + * A list displays a list of interactive rows with support for keyboard + * navigation, single or multiple selection, and row actions. + * + * @public + */ +export const List = (props: ListProps) => { + const { ownProps, restProps, dataAttributes } = useDefinition( + ListDefinition, + props, + ); + const { classes, items, children, renderEmptyState } = ownProps; + + return ( + + {children} + + ); +}; + +/** + * A row within a List. + * + * @public + */ +export const ListRow = (props: ListRowProps) => { + const { ownProps, restProps, dataAttributes } = useDefinition( + ListRowDefinition, + props, + ); + const { classes, children, description, icon, menuItems, customActions } = + ownProps; + + const textValue = typeof children === 'string' ? children : undefined; + + return ( + + {({ isSelected }) => ( + <> + {isSelected && ( +
    + +
    + )} + {icon && ( + + {icon} + + )} +
    + {children} + {description && ( + + {description} + + )} +
    + {customActions && ( +
    {customActions}
    + )} + {menuItems && ( +
    + + } + size="small" + aria-label="More actions" + variant="tertiary" + /> + {menuItems} + +
    + )} + + )} +
    + ); +}; diff --git a/packages/ui/src/components/List/definition.ts b/packages/ui/src/components/List/definition.ts new file mode 100644 index 0000000000..98b9e24fc6 --- /dev/null +++ b/packages/ui/src/components/List/definition.ts @@ -0,0 +1,61 @@ +/* + * 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 { defineComponent } from '../../hooks/useDefinition'; +import type { ListOwnProps, ListRowOwnProps } from './types'; +import styles from './List.module.css'; + +/** + * Component definition for List + * @public + */ +export const ListDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-List', + }, + propDefs: { + items: {}, + children: {}, + renderEmptyState: {}, + className: {}, + }, +}); + +/** + * Component definition for ListRow + * @public + */ +export const ListRowDefinition = defineComponent()({ + styles, + bg: 'consumer', + classNames: { + root: 'bui-ListRow', + check: 'bui-ListRowCheck', + icon: 'bui-ListRowIcon', + label: 'bui-ListRowLabel', + description: 'bui-ListRowDescription', + actions: 'bui-ListRowActions', + }, + propDefs: { + children: {}, + description: {}, + icon: {}, + menuItems: {}, + customActions: {}, + className: {}, + }, +}); diff --git a/packages/ui/src/components/InternalLinkProvider/index.ts b/packages/ui/src/components/List/index.ts similarity index 72% rename from packages/ui/src/components/InternalLinkProvider/index.ts rename to packages/ui/src/components/List/index.ts index d0facb962f..e52739c04e 100644 --- a/packages/ui/src/components/InternalLinkProvider/index.ts +++ b/packages/ui/src/components/List/index.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -export { - InternalLinkProvider, - RoutedContainer, - useRoutingRegistration, - isInternalLink, - createRoutingRegistration, -} from './InternalLinkProvider'; -export type { RoutingContextValue } from './InternalLinkProvider'; +export { List, ListRow } from './List'; +export type { + ListProps, + ListOwnProps, + ListRowProps, + ListRowOwnProps, +} from './types'; +export { ListDefinition, ListRowDefinition } from './definition'; diff --git a/packages/ui/src/components/List/types.ts b/packages/ui/src/components/List/types.ts new file mode 100644 index 0000000000..1bd5ad1037 --- /dev/null +++ b/packages/ui/src/components/List/types.ts @@ -0,0 +1,82 @@ +/* + * 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 { + GridListProps as ReactAriaGridListProps, + GridListItemProps as ReactAriaGridListItemProps, +} from 'react-aria-components'; + +/** + * Own props for the List component. + * + * @public + */ +export type ListOwnProps = { + items?: ReactAriaGridListProps['items']; + children?: ReactAriaGridListProps['children']; + renderEmptyState?: ReactAriaGridListProps['renderEmptyState']; + className?: string; +}; + +/** + * Props for the List component. + * + * @public + */ +export interface ListProps + extends ListOwnProps, + Omit, keyof ListOwnProps> {} + +/** + * Own props for the ListRow component. + * + * @public + */ +export type ListRowOwnProps = { + /** + * The main label content of the row. + */ + children?: React.ReactNode; + /** + * Optional secondary description text. + */ + description?: string; + /** + * Optional icon displayed before the label, rendered in a 32×32px box. + */ + icon?: React.ReactElement; + /** + * Optional menu items rendered inside an automatically managed dropdown menu. + * Pass `MenuItem` nodes here and the component will render the trigger button + * and menu wrapper for you. + */ + menuItems?: React.ReactNode; + /** + * Optional actions rendered in a flex row on the right side of the row, + * e.g. a set of tags. For a dropdown menu, prefer `menuItems`. + */ + customActions?: React.ReactNode; + className?: string; +}; + +/** + * Props for the ListRow component. + * + * @public + */ +export interface ListRowProps + extends ListRowOwnProps, + Omit {} diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index 4967d26551..9acedd106e 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -18,12 +18,13 @@ @layer components { .bui-MenuPopover { + --menu-border-radius: var(--bui-radius-2); display: flex; flex-direction: column; box-shadow: var(--bui-shadow); border: 1px solid var(--bui-border-1); - border-radius: var(--bui-radius-2); - background: var(--bui-bg-popover); + border-radius: var(--menu-border-radius); + background: var(--bui-bg-app); color: var(--bui-fg-primary); outline: none; transition: transform 200ms, opacity 200ms; @@ -55,6 +56,14 @@ } } + .bui-MenuInner { + border-radius: var(--menu-border-radius); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + .bui-MenuContent { max-height: inherit; box-sizing: border-box; @@ -69,6 +78,15 @@ padding-inline: var(--bui-space-1); display: block; + &[data-focus-visible] { + outline: none; + } + + &[data-focus-visible] { + outline: 2px solid var(--bui-ring); + outline-offset: -2px; + } + &[data-focused] .bui-MenuItemWrapper { background: var(--bui-bg-neutral-2); color: var(--bui-fg-primary); diff --git a/packages/ui/src/components/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx index ffe184d4ae..26164f3752 100644 --- a/packages/ui/src/components/Menu/Menu.stories.tsx +++ b/packages/ui/src/components/Menu/Menu.stories.tsx @@ -36,6 +36,7 @@ import { RiShareBoxLine, } from '@remixicon/react'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { useEffect, useState } from 'react'; const meta = preview.meta({ @@ -44,7 +45,9 @@ const meta = preview.meta({ decorators: [ Story => ( - + + + ), ], @@ -198,6 +201,24 @@ export const PreviewLinks = meta.story({ }); export const Opened = meta.story({ + parameters: { layout: 'fullscreen' }, + decorators: [ + Story => ( +
    + +
    + ), + ], args: { ...Preview.input.args, }, diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index e02292ba44..cb6028c45a 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -33,8 +33,18 @@ import { Virtualizer, ListLayout, } from 'react-aria-components'; -import { useStyles } from '../../hooks/useStyles'; -import { MenuDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + MenuDefinition, + MenuListBoxDefinition, + MenuAutocompleteDefinition, + MenuAutocompleteListboxDefinition, + MenuItemDefinition, + MenuListBoxItemDefinition, + MenuSectionDefinition, + MenuSeparatorDefinition, + MenuEmptyStateDefinition, +} from './definition'; import type { MenuTriggerProps, SubmenuTriggerProps, @@ -52,27 +62,18 @@ import { RiCheckLine, RiCloseCircleLine, } from '@remixicon/react'; -import { - isInternalLink, - createRoutingRegistration, -} from '../InternalLinkProvider'; -import styles from './Menu.module.css'; -import clsx from 'clsx'; - -const { RoutingProvider, useRoutingRegistrationEffect } = - createRoutingRegistration(); +import { isInternalLink } from '../../utils/linkUtils'; +import { getNodeText } from '../../analytics/getNodeText'; +import { Box } from '../Box'; +import { BgReset } from '../../hooks/useBg'; // The height will be used for virtualized menus. It should match the size set in CSS for each menu item. const rowHeight = 32; const MenuEmptyState = () => { - const { classNames } = useStyles(MenuDefinition); + const { ownProps } = useDefinition(MenuEmptyStateDefinition, {}); - return ( -
    - No results found. -
    - ); + return
    No results found.
    ; }; /** @public */ @@ -87,162 +88,25 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => { /** @public */ export const Menu = (props: MenuProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - placement = 'bottom start', - virtualized = false, - maxWidth, - maxHeight, - style, - ...rest - } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuDefinition, props); + const { classes, placement, virtualized, maxWidth, maxHeight, style } = + ownProps; let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const menuContent = ( } style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} + {...restProps} /> ); return ( - - - {virtualized ? ( - - {menuContent} - - ) : ( - menuContent - )} - - - ); -}; - -/** @public */ -export const MenuListBox = (props: MenuListBoxProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - selectionMode = 'single', - placement = 'bottom start', - virtualized = false, - maxWidth, - maxHeight, - style, - ...rest - } = cleanedProps; - let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); - - const listBoxContent = ( - - ); - - return ( - - {virtualized ? ( - - {listBoxContent} - - ) : ( - listBoxContent - )} - - ); -}; - -/** @public */ -export const MenuAutocomplete = (props: MenuAutocompleteProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - placement = 'bottom start', - virtualized = false, - maxWidth, - maxHeight, - style, - ...rest - } = cleanedProps; - const { contains } = useFilter({ sensitivity: 'base' }); - let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); - - const menuContent = ( - } - style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} - /> - ); - - return ( - - - - - - - - - + + + {virtualized ? ( ) => { ) : ( menuContent )} - - - + + + + ); +}; + +/** @public */ +export const MenuListBox = (props: MenuListBoxProps) => { + const { ownProps, restProps } = useDefinition(MenuListBoxDefinition, props); + const { + classes, + selectionMode, + placement, + virtualized, + maxWidth, + maxHeight, + style, + } = ownProps; + let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); + + const listBoxContent = ( + + ); + + return ( + + + + {virtualized ? ( + + {listBoxContent} + + ) : ( + listBoxContent + )} + + + + ); +}; + +/** @public */ +export const MenuAutocomplete = (props: MenuAutocompleteProps) => { + const { ownProps, restProps } = useDefinition( + MenuAutocompleteDefinition, + props, + ); + const { + classes, + placement, + virtualized, + maxWidth, + maxHeight, + style, + placeholder, + } = ownProps; + const { contains } = useFilter({ sensitivity: 'base' }); + let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); + + const menuContent = ( + } + style={{ width: newMaxWidth, maxHeight, ...style }} + {...restProps} + /> + ); + + return ( + + + + + + + + + + + {virtualized ? ( + + {menuContent} + + ) : ( + menuContent + )} + + + + ); }; @@ -265,122 +237,107 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { export const MenuAutocompleteListbox = ( props: MenuAutocompleteListBoxProps, ) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); + const { ownProps, restProps } = useDefinition( + MenuAutocompleteListboxDefinition, + props, + ); const { - className, - selectionMode = 'single', - placement = 'bottom start', - virtualized = false, + classes, + selectionMode, + placement, + virtualized, maxWidth, maxHeight, style, - ...rest - } = cleanedProps; + placeholder, + } = ownProps; const { contains } = useFilter({ sensitivity: 'base' }); let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const listBoxContent = ( } selectionMode={selectionMode} style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} + {...restProps} /> ); return ( - - - - + + + + + + + + + + {virtualized ? ( + + {listBoxContent} + + ) : ( + listBoxContent )} - placeholder={props.placeholder || 'Search...'} - /> - - - - - {virtualized ? ( - - {listBoxContent} - - ) : ( - listBoxContent - )} - + + + ); }; /** @public */ export const MenuItem = (props: MenuItemProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - iconStart, - color = 'primary', - children, - href, - ...rest - } = cleanedProps; + const { ownProps, restProps, dataAttributes, analytics } = useDefinition( + MenuItemDefinition, + props, + ); + const { classes, iconStart, children, href } = ownProps; - useRoutingRegistrationEffect(href); + const handleAction = () => { + if (href) { + const text = + restProps['aria-label'] ?? getNodeText(children) ?? String(href); + analytics.captureEvent('click', text, { + attributes: { to: String(href) }, + }); + } + }; // External links open in new tab via window.open instead of client-side routing if (href && !isInternalLink(href)) { return ( window.open(href, '_blank', 'noopener,noreferrer')} - {...rest} + {...restProps} + onAction={() => { + restProps.onAction?.(); + handleAction(); + window.open(href, '_blank', 'noopener,noreferrer'); + }} > -
    -
    +
    +
    {iconStart} {children}
    -
    +
    @@ -390,27 +347,22 @@ export const MenuItem = (props: MenuItemProps) => { return ( { + restProps.onAction?.(); + handleAction(); + }} > -
    -
    +
    +
    {iconStart} {children}
    -
    +
    @@ -420,36 +372,21 @@ export const MenuItem = (props: MenuItemProps) => { /** @public */ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { children, className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition( + MenuListBoxItemDefinition, + props, + ); + const { classes, children } = ownProps; return ( -
    -
    -
    +
    +
    +
    {children} @@ -461,26 +398,12 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { /** @public */ export const MenuSection = (props: MenuSectionProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { children, className, title, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuSectionDefinition, props); + const { classes, children, title } = ownProps; return ( - - - {title} - + + {title} {children} ); @@ -488,17 +411,7 @@ export const MenuSection = (props: MenuSectionProps) => { /** @public */ export const MenuSeparator = (props: MenuSeparatorProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuSeparatorDefinition, props); - return ( - - ); + return ; }; diff --git a/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx b/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx index af68ec9415..7ebd25a063 100644 --- a/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx +++ b/packages/ui/src/components/Menu/MenuAutocomplete.stories.tsx @@ -25,6 +25,7 @@ import { import { Button } from '../..'; import { useState, useEffect } from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/MenuAutocomplete', @@ -32,7 +33,9 @@ const meta = preview.meta({ decorators: [ Story => ( - + + + ), ], diff --git a/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx b/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx index e1c22fbd9e..f3e001c165 100644 --- a/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx +++ b/packages/ui/src/components/Menu/MenuAutocompleteListBox.stories.tsx @@ -27,6 +27,7 @@ import { Button, Flex, Text } from '../..'; import { useEffect, useState } from 'react'; import { Selection } from 'react-aria-components'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/MenuAutocompleteListBox', @@ -34,7 +35,9 @@ const meta = preview.meta({ decorators: [ Story => ( - + + + ), ], diff --git a/packages/ui/src/components/Menu/MenuListBox.stories.tsx b/packages/ui/src/components/Menu/MenuListBox.stories.tsx index 2297598d0c..469a106280 100644 --- a/packages/ui/src/components/Menu/MenuListBox.stories.tsx +++ b/packages/ui/src/components/Menu/MenuListBox.stories.tsx @@ -20,6 +20,7 @@ import { Button, Flex, Text } from '../..'; import { useEffect, useState } from 'react'; import { Selection } from 'react-aria-components'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; const meta = preview.meta({ title: 'Backstage UI/MenuListBox', @@ -27,7 +28,9 @@ const meta = preview.meta({ decorators: [ Story => ( - + + + ), ], diff --git a/packages/ui/src/components/Menu/definition.ts b/packages/ui/src/components/Menu/definition.ts index e4ba0d83ab..076e9ce3ae 100644 --- a/packages/ui/src/components/Menu/definition.ts +++ b/packages/ui/src/components/Menu/definition.ts @@ -14,29 +14,155 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { + MenuOwnProps, + MenuListBoxOwnProps, + MenuAutocompleteOwnProps, + MenuAutocompleteListBoxOwnProps, + MenuItemOwnProps, + MenuListBoxItemOwnProps, + MenuSectionOwnProps, + MenuSeparatorOwnProps, +} from './types'; +import styles from './Menu.module.css'; + +// Shared classNames for all popover-based menu variants +const menuPopoverClassNames = { + root: 'bui-MenuPopover', + inner: 'bui-MenuInner', + content: 'bui-MenuContent', +} as const; + +// Shared classNames for autocomplete menu variants +const menuAutocompleteClassNames = { + ...menuPopoverClassNames, + searchField: 'bui-MenuSearchField', + searchFieldInput: 'bui-MenuSearchFieldInput', + searchFieldClear: 'bui-MenuSearchFieldClear', +} as const; + +// Shared propDefs for all popover-based menu variants +const menuPopoverPropDefs = { + placement: { default: 'bottom start' }, + virtualized: { default: false }, + maxWidth: {}, + maxHeight: {}, + style: {}, + className: {}, +} as const; /** * Component definition for Menu * @public */ -export const MenuDefinition = { +export const MenuDefinition = defineComponent()({ + styles, + classNames: menuPopoverClassNames, + propDefs: menuPopoverPropDefs, +}); + +/** @internal */ +export const MenuListBoxDefinition = defineComponent()({ + styles, + classNames: menuPopoverClassNames, + propDefs: { + ...menuPopoverPropDefs, + selectionMode: { default: 'single' }, + }, +}); + +/** @internal */ +export const MenuAutocompleteDefinition = + defineComponent()({ + styles, + classNames: menuAutocompleteClassNames, + propDefs: { + ...menuPopoverPropDefs, + placeholder: {}, + }, + }); + +/** @internal */ +export const MenuAutocompleteListboxDefinition = + defineComponent()({ + styles, + classNames: menuAutocompleteClassNames, + propDefs: { + ...menuPopoverPropDefs, + placeholder: {}, + selectionMode: { default: 'single' }, + }, + }); + +/** @internal */ +export const MenuItemDefinition = defineComponent()({ + styles, classNames: { - root: 'bui-Menu', - popover: 'bui-MenuPopover', - content: 'bui-MenuContent', - section: 'bui-MenuSection', - sectionHeader: 'bui-MenuSectionHeader', - item: 'bui-MenuItem', - itemListBox: 'bui-MenuItemListBox', - itemListBoxCheck: 'bui-MenuItemListBoxCheck', + root: 'bui-MenuItem', itemWrapper: 'bui-MenuItemWrapper', itemContent: 'bui-MenuItemContent', itemArrow: 'bui-MenuItemArrow', - separator: 'bui-MenuSeparator', - searchField: 'bui-MenuSearchField', - searchFieldInput: 'bui-MenuSearchFieldInput', - searchFieldClear: 'bui-MenuSearchFieldClear', - emptyState: 'bui-MenuEmptyState', }, -} as const satisfies ComponentDefinition; + analytics: true, + propDefs: { + iconStart: {}, + children: {}, + color: { dataAttribute: true, default: 'primary' }, + href: {}, + noTrack: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuListBoxItemDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-MenuItemListBox', + itemWrapper: 'bui-MenuItemWrapper', + itemContent: 'bui-MenuItemContent', + check: 'bui-MenuItemListBoxCheck', + }, + propDefs: { + children: {}, + className: {}, + }, + }); + +/** @internal */ +export const MenuSectionDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-MenuSection', + header: 'bui-MenuSectionHeader', + }, + propDefs: { + title: {}, + children: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuSeparatorDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-MenuSeparator', + }, + propDefs: { + className: {}, + }, + }, +); + +/** @internal */ +export const MenuEmptyStateDefinition = defineComponent<{}>()({ + styles, + classNames: { + root: 'bui-MenuEmptyState', + }, + propDefs: {}, +}); diff --git a/packages/ui/src/components/Menu/index.ts b/packages/ui/src/components/Menu/index.ts index f42de8a7eb..0fbd40c5b5 100644 --- a/packages/ui/src/components/Menu/index.ts +++ b/packages/ui/src/components/Menu/index.ts @@ -31,11 +31,20 @@ export type { MenuTriggerProps, SubmenuTriggerProps, MenuProps, + MenuPopoverOwnProps, + MenuOwnProps, MenuListBoxProps, + MenuListBoxOwnProps, MenuAutocompleteProps, + MenuAutocompleteOwnProps, MenuAutocompleteListBoxProps, + MenuAutocompleteListBoxOwnProps, MenuItemProps, + MenuItemOwnProps, MenuListBoxItemProps, + MenuListBoxItemOwnProps, MenuSectionProps, + MenuSectionOwnProps, MenuSeparatorProps, + MenuSeparatorOwnProps, } from './types'; diff --git a/packages/ui/src/components/Menu/types.ts b/packages/ui/src/components/Menu/types.ts index 6d4852a4b3..8a40897ddd 100644 --- a/packages/ui/src/components/Menu/types.ts +++ b/packages/ui/src/components/Menu/types.ts @@ -32,71 +32,103 @@ export interface MenuTriggerProps extends RAMenuTriggerProps {} /** @public */ export interface SubmenuTriggerProps extends RAMenuSubmenuTriggerProps {} -/** @public */ -export interface MenuProps - extends RAMenuProps, - Omit, 'children'> { +/** + * Common own props shared by all Menu popover variants. + * + * @public + */ +export type MenuPopoverOwnProps = { placement?: RAPopoverProps['placement']; virtualized?: boolean; maxWidth?: string; maxHeight?: string; -} + style?: React.CSSProperties; + className?: string; +}; + +/** @public */ +export type MenuOwnProps = MenuPopoverOwnProps; + +/** @public */ +export interface MenuProps + extends MenuOwnProps, + Omit, keyof MenuOwnProps> {} + +/** @public */ +export type MenuListBoxOwnProps = MenuPopoverOwnProps & { + selectionMode?: RAListBoxProps['selectionMode']; +}; /** @public */ export interface MenuListBoxProps - extends RAListBoxProps, - Omit, 'children'> { - placement?: RAPopoverProps['placement']; - virtualized?: boolean; - maxWidth?: string; - maxHeight?: string; -} + extends MenuListBoxOwnProps, + Omit, keyof MenuListBoxOwnProps> {} + +/** @public */ +export type MenuAutocompleteOwnProps = MenuPopoverOwnProps & { + placeholder?: string; +}; /** @public */ export interface MenuAutocompleteProps - extends RAMenuProps, - Omit, 'children'> { + extends MenuAutocompleteOwnProps, + Omit, keyof MenuAutocompleteOwnProps> {} + +/** @public */ +export type MenuAutocompleteListBoxOwnProps = MenuPopoverOwnProps & { placeholder?: string; - placement?: RAPopoverProps['placement']; - virtualized?: boolean; - maxWidth?: string; - maxHeight?: string; -} + selectionMode?: RAListBoxProps['selectionMode']; +}; /** @public */ export interface MenuAutocompleteListBoxProps - extends RAListBoxProps, - Omit, 'children'> { - placeholder?: string; - placement?: RAPopoverProps['placement']; - virtualized?: boolean; - maxWidth?: string; - maxHeight?: string; -} + extends MenuAutocompleteListBoxOwnProps, + Omit, keyof MenuAutocompleteListBoxOwnProps> {} /** @public */ -export interface MenuItemProps - extends RAMenuItemProps, - Omit { +export type MenuItemOwnProps = { iconStart?: React.ReactNode; children: React.ReactNode; color?: 'primary' | 'danger'; -} + href?: RAMenuItemProps['href']; + noTrack?: boolean; + className?: string; +}; + +/** @public */ +export interface MenuItemProps + extends MenuItemOwnProps, + Omit {} + +/** @public */ +export type MenuListBoxItemOwnProps = { + children: React.ReactNode; + className?: string; +}; /** @public */ export interface MenuListBoxItemProps - extends RAListBoxItemProps, - Omit { + extends MenuListBoxItemOwnProps, + Omit {} + +/** @public */ +export type MenuSectionOwnProps = { + title: string; children: React.ReactNode; -} + className?: string; +}; /** @public */ export interface MenuSectionProps - extends RAMenuSectionProps, - Omit, 'children'> { - title: string; - children: React.ReactNode; -} + extends MenuSectionOwnProps, + Omit, keyof MenuSectionOwnProps> {} /** @public */ -export interface MenuSeparatorProps extends RAMenuSeparatorProps {} +export type MenuSeparatorOwnProps = { + className?: string; +}; + +/** @public */ +export interface MenuSeparatorProps + extends MenuSeparatorOwnProps, + Omit {} diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx index c2cfdd1081..7dd10f8536 100644 --- a/packages/ui/src/components/PasswordField/PasswordField.tsx +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -20,71 +20,45 @@ import { TextField as AriaTextField, Button as RAButton, } from 'react-aria-components'; -import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; import { FieldError } from '../FieldError'; import type { PasswordFieldProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { PasswordFieldDefinition } from './definition'; import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; -import stylesPasswordField from './PasswordField.module.css'; /** @public */ export const PasswordField = forwardRef( (props, ref) => { - const { - label, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - } = props; + const { ownProps, restProps, dataAttributes } = useDefinition( + PasswordFieldDefinition, + props, + ); + const { classes, label, icon, secondaryLabel, placeholder, description } = + ownProps; useEffect(() => { - if (!label && !ariaLabel && !ariaLabelledBy) { + if (!label && !restProps['aria-label'] && !restProps['aria-labelledby']) { console.warn( 'PasswordField requires either a visible label, aria-label, or aria-labelledby for accessibility', ); } - }, [label, ariaLabel, ariaLabelledBy]); - - const { - classNames: classNamesPasswordField, - dataAttributes, - cleanedProps, - } = useStyles(PasswordFieldDefinition, { - size: 'small', - ...props, - }); - - const { - className, - description, - icon, - isRequired, - secondaryLabel, - placeholder, - ...rest - } = cleanedProps; + }, [label, restProps['aria-label'], restProps['aria-labelledby']]); // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = - secondaryLabel || (isRequired ? 'Required' : null); + secondaryLabel || (restProps.isRequired ? 'Required' : null); // Manage secret visibility toggle const [isVisible, setIsVisible] = useState(false); return ( ( description={description} />
    {icon && ( )} ( aria-controls={isVisible ? 'text' : 'password'} aria-expanded={isVisible} onPress={() => setIsVisible(v => !v)} - className={clsx( - classNamesPasswordField.inputVisibility, - stylesPasswordField[classNamesPasswordField.inputVisibility], - )} + className={classes.inputVisibility} > {isVisible ? : } diff --git a/packages/ui/src/components/PasswordField/definition.ts b/packages/ui/src/components/PasswordField/definition.ts index 11272494b4..7133969eff 100644 --- a/packages/ui/src/components/PasswordField/definition.ts +++ b/packages/ui/src/components/PasswordField/definition.ts @@ -14,21 +14,32 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { PasswordFieldOwnProps } from './types'; +import styles from './PasswordField.module.css'; /** * Component definition for PasswordField * @public */ -export const PasswordFieldDefinition = { - classNames: { - root: 'bui-PasswordField', - inputWrapper: 'bui-PasswordFieldInputWrapper', - input: 'bui-PasswordFieldInput', - inputIcon: 'bui-PasswordFieldIcon', - inputVisibility: 'bui-PasswordFieldVisibility', +export const PasswordFieldDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-PasswordField', + inputWrapper: 'bui-PasswordFieldInputWrapper', + input: 'bui-PasswordFieldInput', + inputIcon: 'bui-PasswordFieldIcon', + inputVisibility: 'bui-PasswordFieldVisibility', + }, + propDefs: { + size: { dataAttribute: true, default: 'small' }, + className: {}, + icon: {}, + placeholder: {}, + label: {}, + description: {}, + secondaryLabel: {}, + }, }, - dataAttributes: { - size: ['small', 'medium'] as const, - }, -} as const satisfies ComponentDefinition; +); diff --git a/packages/ui/src/components/PasswordField/types.ts b/packages/ui/src/components/PasswordField/types.ts index 6fade7496a..882b0320e9 100644 --- a/packages/ui/src/components/PasswordField/types.ts +++ b/packages/ui/src/components/PasswordField/types.ts @@ -15,27 +15,36 @@ */ import type { TextFieldProps as AriaTextFieldProps } from 'react-aria-components'; -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import type { Breakpoint } from '../../types'; import type { FieldLabelProps } from '../FieldLabel/types'; /** @public */ -export interface PasswordFieldProps - extends AriaTextFieldProps, - Omit { - /** - * An icon to render before the input - */ - icon?: ReactNode; - +export type PasswordFieldOwnProps = { /** * The size of the password field * @defaultValue 'medium' */ size?: 'small' | 'medium' | Partial>; + className?: string; + + /** + * An icon to render before the input + */ + icon?: ReactNode; + /** * Text to display in the input when it has no value */ placeholder?: string; -} + + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +/** @public */ +export interface PasswordFieldProps + extends Omit, + PasswordFieldOwnProps {} diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css index 037fd00722..f016ef7674 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css +++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css @@ -17,31 +17,11 @@ @layer tokens, base, components, utilities; @layer components { - .bui-PluginHeader { - display: block; - } - - .bui-PluginHeaderToolbar { - &::before { - content: ''; - position: absolute; - top: 0; - left: 0px; - right: 0px; - height: 16px; - background-color: var(--bui-bg-app); - z-index: 0; - } - } - .bui-PluginHeaderToolbarWrapper { - position: relative; - z-index: 1; display: flex; flex-direction: row; align-items: center; justify-content: space-between; - background-color: var(--bui-bg-neutral-1); padding-inline: var(--bui-space-5); border-bottom: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); @@ -77,10 +57,6 @@ } .bui-PluginHeaderToolbarControls { - position: absolute; - right: var(--bui-space-5); - top: 50%; - transform: translateY(-50%); display: flex; flex-direction: row; align-items: center; @@ -90,6 +66,5 @@ .bui-PluginHeaderTabsWrapper { padding-inline: var(--bui-space-3); border-bottom: 1px solid var(--bui-border-1); - background-color: var(--bui-bg-neutral-1); } } diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx index 91cb69b2a8..d0d3475dbb 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx @@ -20,7 +20,7 @@ import { PluginHeader } from './PluginHeader'; import type { HeaderTab } from './types'; import { Button, - HeaderPage, + Header, Container, Text, ButtonIcon, @@ -29,13 +29,14 @@ import { MenuItem, } from '../../'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../provider'; import { RiHeartLine, RiEmotionHappyLine, RiCloudy2Line, RiMore2Line, } from '@remixicon/react'; -import { HeaderPageBreadcrumb } from '../HeaderPage/types'; +import { HeaderBreadcrumb } from '../Header/types'; const meta = preview.meta({ title: 'Backstage UI/PluginHeader', @@ -47,7 +48,9 @@ const meta = preview.meta({ const withRouter = (Story: StoryFn) => ( - + + + ); @@ -117,7 +120,7 @@ const menuItems = [ }, ]; -const breadcrumbs: HeaderPageBreadcrumb[] = [ +const breadcrumbs: HeaderBreadcrumb[] = [ { label: 'Home', href: '/', @@ -246,7 +249,7 @@ export const WithAllOptionsAndTabs = WithCustomActions.extend({ }, }); -export const WithHeaderPage = meta.story({ +export const WithHeader = meta.story({ args: { ...WithAllOptionsAndTabs.input.args, }, @@ -263,7 +266,7 @@ export const WithHeaderPage = meta.story({ } /> - Custom action} @@ -278,7 +281,7 @@ export const WithLayout = meta.story({ render: args => ( <> - Custom action} @@ -293,7 +296,7 @@ export const WithLayoutNoTabs = meta.story({ render: args => ( <> - +
    ), }); @@ -316,7 +319,7 @@ export const WithEverything = meta.story({ } /> - ( - - - - Current URL is mocked to be: /campaigns - - - Notice how the "Campaigns" tab is selected (highlighted) because it - matches the current path. - - + + + + + Current URL is mocked to be: /campaigns + + + Notice how the "Campaigns" tab is selected (highlighted) because it + matches the current path. + + + ), }); @@ -356,16 +361,18 @@ export const WithMockedURLIntegrations = meta.story({ }, render: args => ( - - - - Current URL is mocked to be: /integrations - - - Notice how the "Integrations" tab is selected (highlighted) because it - matches the current path. - - + + + + + Current URL is mocked to be: /integrations + + + Notice how the "Integrations" tab is selected (highlighted) because + it matches the current path. + + + ), }); @@ -376,20 +383,22 @@ export const WithMockedURLNoMatch = meta.story({ }, render: args => ( - - - - Current URL is mocked to be: /some-other-page - - - No tab is selected because the current path doesn't match any tab's - href. - - - Tabs without href (like "Overview", "Checks", "Tracks") fall back to - React Aria's internal state. - - + + + + + Current URL is mocked to be: /some-other-page + + + No tab is selected because the current path doesn't match any tab's + href. + + + Tabs without href (like "Overview", "Checks", "Tracks") fall back to + React Aria's internal state. + + + ), }); @@ -424,32 +433,34 @@ export const WithTabsMatchingStrategies = meta.story({ }, render: args => ( - - - - Current URL: /mentorship/events - -
    - - Notice how the "Mentorship" tab is active even though we're on a - nested route. This is because it uses{' '} - matchStrategy="prefix". - -
    - - • Home: exact matching (default) - not active - - - • Mentorship: prefix matching - IS active (URL starts - with /mentorship) - - - • Catalog: prefix matching - not active - - - • Settings: exact matching (default) - not active - -
    + + + + + Current URL: /mentorship/events + +
    + + Notice how the "Mentorship" tab is active even though we're on a + nested route. This is because it uses{' '} + matchStrategy="prefix". + +
    + + • Home: exact matching (default) - not active + + + • Mentorship: prefix matching - IS active (URL + starts with /mentorship) + + + • Catalog: prefix matching - not active + + + • Settings: exact matching (default) - not active + +
    +
    ), }); @@ -477,18 +488,20 @@ export const WithTabsExactMatching = meta.story({ }, render: args => ( - - - - Current URL: /mentorship/events - -
    - - With default exact matching, only the "Events" tab is active because - it exactly matches the current URL. The "Mentorship" tab is not active - even though the URL is under /mentorship. - -
    + + + + + Current URL: /mentorship/events + +
    + + With default exact matching, only the "Events" tab is active because + it exactly matches the current URL. The "Mentorship" tab is not + active even though the URL is under /mentorship. + +
    +
    ), }); @@ -519,33 +532,36 @@ export const WithTabsPrefixMatchingDeep = meta.story({ }, render: args => ( - - - - Current URL: /catalog/users/john/details - -
    - - Active tab is Users because: - -
      -
    • - Catalog: Matches since URL starts with /catalog -
    • -
    • - Users: Is active since URL starts with - /catalog/users, and is more specific (has more url segments) than - "Catalog" -
    • -
    • - Components: not active (URL doesn't start with - /catalog/components) -
    • -
    - - This demonstrates how prefix matching works with deeply nested routes. - -
    + + + + + Current URL: /catalog/users/john/details + +
    + + Active tab is Users because: + +
      +
    • + Catalog: Matches since URL starts with /catalog +
    • +
    • + Users: Is active since URL starts with + /catalog/users, and is more specific (has more url segments) than + "Catalog" +
    • +
    • + Components: not active (URL doesn't start with + /catalog/components) +
    • +
    + + This demonstrates how prefix matching works with deeply nested + routes. + +
    +
    ), }); diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx index 2ed663d61c..d153a3af18 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx @@ -15,15 +15,16 @@ */ import type { PluginHeaderProps } from './types'; -import { PluginHeaderToolbar } from './PluginHeaderToolbar'; import { Tabs, TabList, Tab } from '../Tabs'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { PluginHeaderDefinition } from './definition'; import { type NavigateOptions } from 'react-router-dom'; -import { useRef } from 'react'; +import { Children, useMemo, useRef } from 'react'; import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; -import styles from './PluginHeader.module.css'; -import clsx from 'clsx'; +import { Box } from '../Box'; +import { Link } from 'react-aria-components'; +import { RiShapesLine } from '@remixicon/react'; +import { Text } from '../Text'; declare module 'react-aria-components' { interface RouterConfig { @@ -38,71 +39,119 @@ declare module 'react-aria-components' { * @public */ export const PluginHeader = (props: PluginHeaderProps) => { - const { classNames, cleanedProps } = useStyles(PluginHeaderDefinition, props); + const { ownProps } = useDefinition(PluginHeaderDefinition, props); const { - className, + classes, tabs, icon, title, titleLink, customActions, onTabSelectionChange, - } = cleanedProps; + } = ownProps; const hasTabs = tabs && tabs.length > 0; const headerRef = useRef(null); + const toolbarWrapperRef = useRef(null); + const toolbarContentRef = useRef(null); + const toolbarControlsRef = useRef(null); + const animationFrameRef = useRef(undefined); + const lastAppliedHeightRef = useRef(undefined); + + const actionChildren = useMemo(() => { + return Children.toArray(customActions); + }, [customActions]); useIsomorphicLayoutEffect(() => { const el = headerRef.current; - if (!el) return undefined; + if (!el) { + return undefined; + } - const updateHeight = () => { - const height = el.offsetHeight; + const cancelScheduledUpdate = () => { + if (animationFrameRef.current === undefined) { + return; + } + + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; + }; + + const applyHeight = (height: number) => { + if (lastAppliedHeightRef.current === height) { + return; + } + + lastAppliedHeightRef.current = height; document.documentElement.style.setProperty( '--bui-header-height', `${height}px`, ); }; - // Set height once immediately - updateHeight(); + const scheduleHeightUpdate = () => { + cancelScheduledUpdate(); + animationFrameRef.current = requestAnimationFrame(() => { + animationFrameRef.current = undefined; + applyHeight(el.offsetHeight); + }); + }; + + // Set height once immediately so the initial layout is correct. + applyHeight(el.offsetHeight); // Observe for resize changes if ResizeObserver is available // (not present in Jest/jsdom by default) if (typeof ResizeObserver === 'undefined') { return () => { + cancelScheduledUpdate(); + lastAppliedHeightRef.current = undefined; document.documentElement.style.removeProperty('--bui-header-height'); }; } - const observer = new ResizeObserver(updateHeight); + const observer = new ResizeObserver(() => { + scheduleHeightUpdate(); + }); observer.observe(el); return () => { observer.disconnect(); + cancelScheduledUpdate(); + lastAppliedHeightRef.current = undefined; document.documentElement.style.removeProperty('--bui-header-height'); }; }, []); + const titleContent = ( + <> +
    {icon || }
    + {title || 'Your plugin'} + + ); + return ( -
    - +
    +
    +
    +
    + + {titleLink ? ( + + {titleContent} + + ) : ( +
    {titleContent}
    + )} +
    +
    +
    + {actionChildren} +
    +
    +
    {tabs && ( -
    + {tabs?.map(tab => ( @@ -117,7 +166,7 @@ export const PluginHeader = (props: PluginHeaderProps) => { ))} -
    + )}
    ); diff --git a/packages/ui/src/components/PluginHeader/PluginHeaderToolbar.tsx b/packages/ui/src/components/PluginHeader/PluginHeaderToolbar.tsx deleted file mode 100644 index 607049f497..0000000000 --- a/packages/ui/src/components/PluginHeader/PluginHeaderToolbar.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 { Link } from 'react-aria-components'; -import { useStyles } from '../../hooks/useStyles'; -import { PluginHeaderDefinition } from './definition'; -import { useRef } from 'react'; -import { RiShapesLine } from '@remixicon/react'; -import type { PluginHeaderToolbarProps } from './types'; -import { Text } from '../Text'; -import styles from './PluginHeader.module.css'; -import clsx from 'clsx'; - -/** - * A component that renders the toolbar section of a plugin header. - * - * @internal - */ -export const PluginHeaderToolbar = (props: PluginHeaderToolbarProps) => { - const { classNames, cleanedProps } = useStyles(PluginHeaderDefinition, props); - const { className, icon, title, titleLink, customActions, hasTabs } = - cleanedProps; - - // Refs for collision detection - const toolbarWrapperRef = useRef(null); - const toolbarContentRef = useRef(null); - const toolbarControlsRef = useRef(null); - - const titleContent = ( - <> -
    - {icon || } -
    - {title || 'Your plugin'} - - ); - - return ( -
    -
    -
    - - {titleLink ? ( - - {titleContent} - - ) : ( -
    - {titleContent} -
    - )} -
    -
    -
    - {customActions} -
    -
    -
    - ); -}; diff --git a/packages/ui/src/components/PluginHeader/definition.ts b/packages/ui/src/components/PluginHeader/definition.ts index 39de7a0443..c9a35aa956 100644 --- a/packages/ui/src/components/PluginHeader/definition.ts +++ b/packages/ui/src/components/PluginHeader/definition.ts @@ -14,13 +14,16 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { PluginHeaderOwnProps } from './types'; +import styles from './PluginHeader.module.css'; /** * Component definition for PluginHeader * @public */ -export const PluginHeaderDefinition = { +export const PluginHeaderDefinition = defineComponent()({ + styles, classNames: { root: 'bui-PluginHeader', toolbar: 'bui-PluginHeaderToolbar', @@ -29,6 +32,15 @@ export const PluginHeaderDefinition = { toolbarControls: 'bui-PluginHeaderToolbarControls', toolbarIcon: 'bui-PluginHeaderToolbarIcon', toolbarName: 'bui-PluginHeaderToolbarName', - tabsWrapper: 'bui-PluginHeaderTabsWrapper', + tabs: 'bui-PluginHeaderTabsWrapper', }, -} as const satisfies ComponentDefinition; + propDefs: { + icon: {}, + title: {}, + titleLink: {}, + customActions: {}, + tabs: {}, + onTabSelectionChange: {}, + className: {}, + }, +}); diff --git a/packages/ui/src/components/PluginHeader/index.tsx b/packages/ui/src/components/PluginHeader/index.tsx index 306910f516..3648ebe920 100644 --- a/packages/ui/src/components/PluginHeader/index.tsx +++ b/packages/ui/src/components/PluginHeader/index.tsx @@ -16,4 +16,8 @@ export { PluginHeader } from './PluginHeader'; export { PluginHeaderDefinition } from './definition'; -export type { PluginHeaderProps, HeaderTab } from './types'; +export type { + PluginHeaderOwnProps, + PluginHeaderProps, + HeaderTab, +} from './types'; diff --git a/packages/ui/src/components/PluginHeader/types.ts b/packages/ui/src/components/PluginHeader/types.ts index 21021cdbaf..9048b33699 100644 --- a/packages/ui/src/components/PluginHeader/types.ts +++ b/packages/ui/src/components/PluginHeader/types.ts @@ -18,11 +18,11 @@ import { TabsProps } from 'react-aria-components'; import { TabMatchStrategy } from '../Tabs'; /** - * Props for the {@link PluginHeader} component. + * Own props for the {@link PluginHeader} component. * * @public */ -export interface PluginHeaderProps { +export interface PluginHeaderOwnProps { icon?: React.ReactNode; title?: string; titleLink?: string; @@ -32,6 +32,13 @@ export interface PluginHeaderProps { className?: string; } +/** + * Props for the {@link PluginHeader} component. + * + * @public + */ +export interface PluginHeaderProps extends PluginHeaderOwnProps {} + /** * Represents a tab item in the header navigation. * @@ -48,17 +55,3 @@ export interface HeaderTab { */ matchStrategy?: TabMatchStrategy; } - -/** - * Props for the PluginHeaderToolbar component. - * - * @internal - */ -export interface PluginHeaderToolbarProps { - icon?: PluginHeaderProps['icon']; - title?: PluginHeaderProps['title']; - titleLink?: PluginHeaderProps['titleLink']; - customActions?: PluginHeaderProps['customActions']; - hasTabs?: boolean; - className?: string; -} diff --git a/packages/ui/src/components/Popover/Popover.module.css b/packages/ui/src/components/Popover/Popover.module.css index 69d2acffcf..cdd323817c 100644 --- a/packages/ui/src/components/Popover/Popover.module.css +++ b/packages/ui/src/components/Popover/Popover.module.css @@ -18,9 +18,10 @@ @layer components { .bui-Popover { + --popover-border-radius: var(--bui-radius-3); box-shadow: var(--bui-shadow); - border-radius: var(--bui-radius-3); - background: var(--bui-bg-popover); + border-radius: var(--popover-border-radius); + background: var(--bui-bg-app); border: 1px solid var(--bui-border-1); forced-color-adjust: none; outline: none; @@ -76,6 +77,7 @@ padding: var(--bui-space-4); flex: 1 1 auto; min-height: 0; + border-radius: var(--popover-border-radius); } .bui-PopoverArrow { @@ -89,11 +91,14 @@ we split the stroke and fill across separate elements in order to guarantee that the stroke is always overlaying a consistent color. */ - path:nth-child(1) { - fill: var(--bui-bg-popover); + use:nth-of-type(1) { + fill: var(--bui-bg-app); + } + use:nth-of-type(2) { + fill: var(--bui-bg-neutral-1); } - path:nth-child(2) { + path { fill: var(--bui-border-1); } diff --git a/packages/ui/src/components/Popover/Popover.stories.tsx b/packages/ui/src/components/Popover/Popover.stories.tsx index ea92bf9d5c..0a0e68b572 100644 --- a/packages/ui/src/components/Popover/Popover.stories.tsx +++ b/packages/ui/src/components/Popover/Popover.stories.tsx @@ -19,6 +19,7 @@ import { Button } from '../Button/Button'; import { DialogTrigger } from '../Dialog/Dialog'; import { Text } from '../Text/Text'; import { Flex } from '../Flex/Flex'; +import { Box } from '../Box'; const meta = preview.meta({ title: 'Backstage UI/Popover', @@ -74,6 +75,24 @@ export const Default = meta.story({ }); export const IsOpen = Default.extend({ + parameters: { layout: 'fullscreen' }, + decorators: [ + Story => ( +
    + +
    + ), + ], args: { isOpen: true, }, @@ -189,6 +208,9 @@ export const WithRichContent = Default.extend({ This is a popover with rich content. It can contain multiple elements and formatted text. + + You can also use the automatic bg system inside it. + diff --git a/packages/ui/src/components/Select/SelectListBox.tsx b/packages/ui/src/components/Select/SelectListBox.tsx index 3b14292729..29e2238eb2 100644 --- a/packages/ui/src/components/Select/SelectListBox.tsx +++ b/packages/ui/src/components/Select/SelectListBox.tsx @@ -16,10 +16,8 @@ import { ListBox, ListBoxItem, Text } from 'react-aria-components'; import { RiCheckLine } from '@remixicon/react'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; -import { SelectDefinition } from './definition'; -import styles from './Select.module.css'; +import { useDefinition } from '../../hooks/useDefinition'; +import { SelectListBoxDefinition } from './definition'; import type { Option } from './types'; interface SelectListBoxProps { @@ -27,42 +25,30 @@ interface SelectListBoxProps { } const NoResults = () => { - const { classNames } = useStyles(SelectDefinition); + const { ownProps } = useDefinition(SelectListBoxDefinition, {}); + const { classes } = ownProps; - return ( -
    - No results found. -
    - ); + return
    No results found.
    ; }; -export function SelectListBox({ options, ...props }: SelectListBoxProps) { - const { classNames } = useStyles(SelectDefinition, props); +export function SelectListBox(props: SelectListBoxProps) { + const { ownProps } = useDefinition(SelectListBoxDefinition, props); + const { classes, options } = ownProps; + return ( - } - > + }> {options?.map(option => ( -
    +
    - + {option.label} diff --git a/packages/ui/src/components/Select/SelectTrigger.tsx b/packages/ui/src/components/Select/SelectTrigger.tsx index 08100da273..ddb7601fa7 100644 --- a/packages/ui/src/components/Select/SelectTrigger.tsx +++ b/packages/ui/src/components/Select/SelectTrigger.tsx @@ -17,25 +17,25 @@ import { ReactNode } from 'react'; import { Button, SelectValue } from 'react-aria-components'; import { RiArrowDownSLine } from '@remixicon/react'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; -import { SelectDefinition } from './definition'; -import styles from './Select.module.css'; +import { useDefinition } from '../../hooks/useDefinition'; +import { SelectTriggerDefinition } from './definition'; interface SelectTriggerProps { icon?: ReactNode; } -export function SelectTrigger({ icon }: SelectTriggerProps) { - const { classNames } = useStyles(SelectDefinition); +export function SelectTrigger(props: SelectTriggerProps) { + const { ownProps, dataAttributes } = useDefinition( + SelectTriggerDefinition, + props, + ); + const { classes, icon } = ownProps; return ( - diff --git a/packages/ui/src/components/Select/definition.ts b/packages/ui/src/components/Select/definition.ts index 5a085cc770..937e015098 100644 --- a/packages/ui/src/components/Select/definition.ts +++ b/packages/ui/src/components/Select/definition.ts @@ -14,29 +14,94 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { + SelectOwnProps, + SelectTriggerOwnProps, + SelectContentOwnProps, + SelectListBoxOwnProps, +} from './types'; +import styles from './Select.module.css'; /** * Component definition for Select * @public */ -export const SelectDefinition = { +export const SelectDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Select', popover: 'bui-SelectPopover', - trigger: 'bui-SelectTrigger', - chevron: 'bui-SelectTriggerChevron', - value: 'bui-SelectValue', - list: 'bui-SelectList', - item: 'bui-SelectItem', - itemIndicator: 'bui-SelectItemIndicator', - itemLabel: 'bui-SelectItemLabel', - searchWrapper: 'bui-SelectSearchWrapper', - search: 'bui-SelectSearch', - searchClear: 'bui-SelectSearchClear', - noResults: 'bui-SelectNoResults', }, - dataAttributes: { - size: ['small', 'medium'] as const, + propDefs: { + icon: {}, + size: { dataAttribute: true, default: 'small' }, + options: {}, + searchable: {}, + searchPlaceholder: {}, + label: {}, + secondaryLabel: {}, + description: {}, + isRequired: {}, + className: {}, }, -} as const satisfies ComponentDefinition; +}); + +/** + * Component definition for SelectTrigger + * @internal + */ +export const SelectTriggerDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-SelectTrigger', + chevron: 'bui-SelectTriggerChevron', + value: 'bui-SelectValue', + }, + bg: 'consumer', + propDefs: { + icon: {}, + }, + }, +); + +/** + * Component definition for SelectContent + * @internal + */ +export const SelectContentDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-SelectSearchWrapper', + search: 'bui-SelectSearch', + searchClear: 'bui-SelectSearchClear', + }, + propDefs: { + searchable: {}, + searchPlaceholder: { default: 'Search...' }, + options: {}, + }, + }, +); + +/** + * Component definition for SelectListBox + * @internal + */ +export const SelectListBoxDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-SelectList', + item: 'bui-SelectItem', + itemIndicator: 'bui-SelectItemIndicator', + itemLabel: 'bui-SelectItemLabel', + noResults: 'bui-SelectNoResults', + }, + propDefs: { + options: {}, + }, + }, +); diff --git a/packages/ui/src/components/Select/types.ts b/packages/ui/src/components/Select/types.ts index 436cdeea17..c9de97a089 100644 --- a/packages/ui/src/components/Select/types.ts +++ b/packages/ui/src/components/Select/types.ts @@ -23,9 +23,7 @@ import type { FieldLabelProps } from '../FieldLabel/types'; export type Option = { value: string; label: string; disabled?: boolean }; /** @public */ -export interface SelectProps - extends AriaSelectProps, - Omit { +export type SelectOwnProps = { /** * An icon to render before the input */ @@ -55,9 +53,37 @@ export interface SelectProps */ searchPlaceholder?: string; + label?: FieldLabelProps['label']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; + description?: FieldLabelProps['description']; + isRequired?: boolean; + className?: string; +}; + +/** @public */ +export interface SelectProps + extends SelectOwnProps, + Omit, keyof SelectOwnProps> { /** * Selection mode, single or multiple * @defaultvalue 'single' */ selectionMode?: T; } + +/** @internal */ +export interface SelectTriggerOwnProps { + icon?: SelectOwnProps['icon']; +} + +/** @internal */ +export interface SelectContentOwnProps { + searchable?: boolean; + searchPlaceholder?: string; + options?: SelectOwnProps['options']; +} + +/** @internal */ +export interface SelectListBoxOwnProps { + options?: SelectOwnProps['options']; +} diff --git a/packages/ui/src/components/Skeleton/Skeleton.tsx b/packages/ui/src/components/Skeleton/Skeleton.tsx index c85a9b23cb..be8c430789 100644 --- a/packages/ui/src/components/Skeleton/Skeleton.tsx +++ b/packages/ui/src/components/Skeleton/Skeleton.tsx @@ -14,32 +14,28 @@ * limitations under the License. */ -import { useStyles } from '../../hooks/useStyles'; +import type { SkeletonProps } from './types'; +import { useDefinition } from '../../hooks/useDefinition'; import { SkeletonDefinition } from './definition'; -import { SkeletonProps } from './types'; -import styles from './Skeleton.module.css'; -import clsx from 'clsx'; /** @public */ export const Skeleton = (props: SkeletonProps) => { - const { classNames, cleanedProps } = useStyles(SkeletonDefinition, { - width: 80, - height: 24, - rounded: false, - ...props, - }); - const { className, width, height, rounded, style, ...rest } = cleanedProps; + const { ownProps, restProps, dataAttributes } = useDefinition( + SkeletonDefinition, + props, + ); + const { classes, width, height, style } = ownProps; return (
    ); }; diff --git a/packages/ui/src/components/Skeleton/definition.ts b/packages/ui/src/components/Skeleton/definition.ts index 42c4d6c444..ec2950de8a 100644 --- a/packages/ui/src/components/Skeleton/definition.ts +++ b/packages/ui/src/components/Skeleton/definition.ts @@ -14,14 +14,24 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { SkeletonOwnProps } from './types'; +import styles from './Skeleton.module.css'; /** * Component definition for Skeleton * @public */ -export const SkeletonDefinition = { +export const SkeletonDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Skeleton', }, -} as const satisfies ComponentDefinition; + propDefs: { + width: { default: 80 }, + height: { default: 24 }, + rounded: { dataAttribute: true, default: false }, + className: {}, + style: {}, + }, +}); diff --git a/packages/ui/src/components/Skeleton/index.tsx b/packages/ui/src/components/Skeleton/index.tsx index ab44364f8b..93bdc1cc65 100644 --- a/packages/ui/src/components/Skeleton/index.tsx +++ b/packages/ui/src/components/Skeleton/index.tsx @@ -15,5 +15,5 @@ */ export { Skeleton } from './Skeleton'; -export type { SkeletonProps } from './types'; +export type { SkeletonOwnProps, SkeletonProps } from './types'; export { SkeletonDefinition } from './definition'; diff --git a/packages/ui/src/components/Skeleton/types.ts b/packages/ui/src/components/Skeleton/types.ts index 136873ed27..5d9f644677 100644 --- a/packages/ui/src/components/Skeleton/types.ts +++ b/packages/ui/src/components/Skeleton/types.ts @@ -14,11 +14,18 @@ * limitations under the License. */ -import { ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; /** @public */ -export interface SkeletonProps extends ComponentProps<'div'> { +export type SkeletonOwnProps = { width?: number | string; height?: number | string; rounded?: boolean; -} + className?: string; + style?: React.CSSProperties; +}; + +/** @public */ +export interface SkeletonProps + extends Omit, 'children' | 'className' | 'style'>, + SkeletonOwnProps {} diff --git a/packages/ui/src/components/Switch/Switch.tsx b/packages/ui/src/components/Switch/Switch.tsx index b50db839df..892dbba82a 100644 --- a/packages/ui/src/components/Switch/Switch.tsx +++ b/packages/ui/src/components/Switch/Switch.tsx @@ -17,26 +17,18 @@ import { forwardRef } from 'react'; import { Switch as AriaSwitch } from 'react-aria-components'; import type { SwitchProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { SwitchDefinition } from './definition'; -import styles from './Switch.module.css'; -import clsx from 'clsx'; /** @public */ export const Switch = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles(SwitchDefinition, props); - const { className, label, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(SwitchDefinition, props); + const { classes, label } = ownProps; return ( - -
    + +
    {label} ); diff --git a/packages/ui/src/components/Switch/definition.ts b/packages/ui/src/components/Switch/definition.ts index c8dd004991..30698eb55e 100644 --- a/packages/ui/src/components/Switch/definition.ts +++ b/packages/ui/src/components/Switch/definition.ts @@ -14,15 +14,22 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { SwitchOwnProps } from './types'; +import styles from './Switch.module.css'; /** * Component definition for Switch * @public */ -export const SwitchDefinition = { +export const SwitchDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Switch', indicator: 'bui-SwitchIndicator', }, -} as const satisfies ComponentDefinition; + propDefs: { + label: {}, + className: {}, + }, +}); diff --git a/packages/ui/src/components/Switch/types.ts b/packages/ui/src/components/Switch/types.ts index 725671edd5..e8524fd447 100644 --- a/packages/ui/src/components/Switch/types.ts +++ b/packages/ui/src/components/Switch/types.ts @@ -17,9 +17,15 @@ import type { SwitchProps as AriaSwitchProps } from 'react-aria-components'; /** @public */ -export interface SwitchProps extends AriaSwitchProps { +export type SwitchOwnProps = { /** * The label of the switch */ label?: string; -} + className?: string; +}; + +/** @public */ +export interface SwitchProps + extends Omit, + SwitchOwnProps {} diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index 0faa256e06..62723db6c8 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -17,14 +17,31 @@ @layer tokens, base, components, utilities; @layer components { + .bui-TableWrapper { + display: flex; + flex-direction: column; + } + + .bui-TableResizableContainer { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; + } + .bui-Table { width: 100%; caption-side: bottom; border-collapse: collapse; table-layout: fixed; transition: opacity 0.2s ease-in-out; + overflow: auto; + flex: 1; + min-height: 0; - &[data-stale='true'] { + &[data-stale='true'], + &[data-loading='true'] { opacity: 0.6; } } @@ -52,6 +69,13 @@ gap: var(--bui-space-1); } + .bui-TableHeadLabel { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + .bui-TableHeadSortButton { cursor: pointer; user-select: none; @@ -83,25 +107,29 @@ .bui-TableRow { border-bottom: 1px solid var(--bui-border-2); transition: color 0.2s ease-in-out; + cursor: default; &:hover { - background-color: var(--bui-bg-tint-hover); + background-color: var(--bui-bg-neutral-1-hover); } &[data-selected] { - background-color: var(--bui-bg-tint-pressed); + background-color: var(--bui-bg-neutral-1-pressed); } &[data-pressed] { - background-color: var(--bui-bg-tint-pressed); + background-color: var(--bui-bg-neutral-1-pressed); + } + + &[data-href], + &[data-selection-mode], + &[data-react-aria-pressable='true'] { + cursor: pointer; } &[data-disabled] { - background-color: var(--bui-bg-tint-disabled); - } - - &[data-react-aria-pressable='true'] { - cursor: pointer; + background-color: var(--bui-bg-neutral-1-disabled); + cursor: not-allowed; } } diff --git a/packages/ui/src/components/Table/components/Cell.tsx b/packages/ui/src/components/Table/components/Cell.tsx index 809b224a07..b5fc96d520 100644 --- a/packages/ui/src/components/Table/components/Cell.tsx +++ b/packages/ui/src/components/Table/components/Cell.tsx @@ -14,29 +14,16 @@ * limitations under the License. */ -import clsx from 'clsx'; import { Cell as ReactAriaCell } from 'react-aria-components'; import type { CellProps } from '../types'; -import { useStyles } from '../../../hooks/useStyles'; -import { TableDefinition } from '../definition'; -import styles from '../Table.module.css'; +import { useDefinition } from '../../../hooks/useDefinition'; +import { CellDefinition } from '../definition'; /** @public */ const Cell = (props: CellProps) => { - const { classNames, cleanedProps } = useStyles(TableDefinition, { - color: 'primary' as const, - ...props, - }); - const { className, children, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(CellDefinition, props); - return ( - - {children} - - ); + return ; }; Cell.displayName = 'Cell'; diff --git a/packages/ui/src/components/Table/components/CellProfile.tsx b/packages/ui/src/components/Table/components/CellProfile.tsx index 4abef56e01..0565fff909 100644 --- a/packages/ui/src/components/Table/components/CellProfile.tsx +++ b/packages/ui/src/components/Table/components/CellProfile.tsx @@ -14,45 +14,26 @@ * limitations under the License. */ -import clsx from 'clsx'; import { CellProfileProps } from '../types'; import { Text } from '../../Text/Text'; import { Link } from '../../Link/Link'; import { Avatar } from '../../Avatar'; -import { useStyles } from '../../../hooks/useStyles'; -import { TableDefinition } from '../definition'; +import { useDefinition } from '../../../hooks/useDefinition'; +import { CellProfileDefinition } from '../definition'; import { Cell as ReactAriaCell } from 'react-aria-components'; -import styles from '../Table.module.css'; /** @public */ export const CellProfile = (props: CellProfileProps) => { - const { classNames, cleanedProps } = useStyles(TableDefinition, { - color: 'primary' as const, - ...props, - }); - const { className, src, name, href, description, color, ...rest } = - cleanedProps; + const { ownProps, restProps } = useDefinition(CellProfileDefinition, props); + const { classes, src, name, href, description, color } = ownProps; return ( - -
    + +
    {src && name && ( )} -
    +
    {name && href ? ( {name} ) : ( diff --git a/packages/ui/src/components/Table/components/CellText.tsx b/packages/ui/src/components/Table/components/CellText.tsx index 8855c92e80..30b02b1e44 100644 --- a/packages/ui/src/components/Table/components/CellText.tsx +++ b/packages/ui/src/components/Table/components/CellText.tsx @@ -14,48 +14,23 @@ * limitations under the License. */ -import clsx from 'clsx'; import { Text } from '../../Text'; import { Link } from '../../Link'; import { Cell as ReactAriaCell } from 'react-aria-components'; import type { CellTextProps } from '../types'; -import { useStyles } from '../../../hooks/useStyles'; -import { TableDefinition } from '../definition'; -import styles from '../Table.module.css'; +import { useDefinition } from '../../../hooks/useDefinition'; +import { CellTextDefinition } from '../definition'; /** @public */ const CellText = (props: CellTextProps) => { - const { classNames, cleanedProps } = useStyles(TableDefinition, { - color: 'primary' as const, - ...props, - }); - const { className, title, description, color, leadingIcon, href, ...rest } = - cleanedProps; + const { ownProps, restProps } = useDefinition(CellTextDefinition, props); + const { classes, title, description, color, leadingIcon, href } = ownProps; return ( - -
    - {leadingIcon && ( -
    - {leadingIcon} -
    - )} -
    + +
    + {leadingIcon &&
    {leadingIcon}
    } +
    {href ? ( { - const { classNames, cleanedProps } = useStyles(TableDefinition, props); - const { className, children, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(ColumnDefinition, props); + const { classes, children } = ownProps; return ( - + {({ allowsSorting }) => ( -
    - {children} +
    + {children} {allowsSorting && ( -
    + ); + }, +}; + +export const VirtualizedWithCustomRowHeight: Story = { + render: () => { + const largeData = Array.from({ length: 500 }, (_, i) => ({ + id: String(i), + name: `Service ${i}`, + owner: { name: `Team ${i % 10}` }, + type: ['service', 'website', 'library'][i % 3], + lifecycle: ['production', 'experimental'][i % 2], + description: `Description for service ${i}`, + })); + + const columns: ColumnConfig<(typeof largeData)[0]>[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => largeData, + paginationOptions: { pageSize: 50 }, + }); + + return ( +
    + ); + }, +}; + +export const VirtualizedWithEstimatedRowHeight: Story = { + render: () => { + const largeData = Array.from({ length: 500 }, (_, i) => ({ + id: String(i), + name: `Service ${i}`, + owner: { name: `Team ${i % 10}` }, + type: ['service', 'website', 'library'][i % 3], + lifecycle: ['production', 'experimental'][i % 2], + description: + i % 5 === 0 + ? `This is a much longer description for service ${i} that spans multiple lines to demonstrate variable height row rendering in the virtualized table` + : `Description for service ${i}`, + })); + + const columns: ColumnConfig<(typeof largeData)[0]>[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + cell: item => , + }, + { + id: 'type', + label: 'Type', + cell: item => , + }, + ]; + + const { tableProps } = useTable({ + mode: 'complete', + getData: () => largeData, + paginationOptions: { pageSize: 50 }, + }); + + return ( +
    + ); + }, +}; + // Type filter interface for ComprehensiveServerSide story interface TypeFilter { type: string | null; diff --git a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx index 3ec6af5789..23810660f1 100644 --- a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx +++ b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx @@ -115,7 +115,8 @@ export const NoPagination: Story = { }, { id: 'owner', - label: 'Owner', + label: 'Owner of the component or service in the organization', + defaultWidth: 120, cell: item => , }, { diff --git a/packages/ui/src/components/Table/stories/utils.tsx b/packages/ui/src/components/Table/stories/utils.tsx index 45bc6341ad..130469e5d7 100644 --- a/packages/ui/src/components/Table/stories/utils.tsx +++ b/packages/ui/src/components/Table/stories/utils.tsx @@ -17,6 +17,7 @@ import type { Meta } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../../../provider'; import { CellText, type ColumnConfig } from '..'; // Selection demo data @@ -47,7 +48,9 @@ export const tableStoriesMeta = { decorators: [ (Story: () => JSX.Element) => ( - + + + ), ], diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 6d94d58fe6..3cfbdff34e 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -18,6 +18,9 @@ import { CellProps as ReactAriaCellProps, ColumnProps as ReactAriaColumnProps, TableProps as ReactAriaTableProps, + TableHeaderProps as ReactAriaTableHeaderProps, + TableBodyProps as ReactAriaTableBodyProps, + RowProps as ReactAriaRowProps, } from 'react-aria-components'; import type { ReactElement, ReactNode } from 'react'; import type { SortDescriptor as ReactStatelySortDescriptor } from 'react-stately'; @@ -37,35 +40,138 @@ export interface SortState { } /** @public */ -export interface CellProps extends ReactAriaCellProps {} +export type TableRootOwnProps = { + stale?: boolean; + loading?: boolean; +}; /** @public */ -export interface CellTextProps extends ReactAriaCellProps { +export interface TableRootProps + extends TableRootOwnProps, + Omit {} + +/** @public */ +export type TableHeaderOwnProps = { + columns?: ReactAriaTableHeaderProps['columns']; + children?: ReactAriaTableHeaderProps['children']; +}; + +/** @public */ +export interface TableHeaderProps + extends TableHeaderOwnProps, + Omit, keyof TableHeaderOwnProps> {} + +/** @public */ +export type TableBodyOwnProps = {}; + +/** @public */ +export interface TableBodyProps + extends TableBodyOwnProps, + Omit, keyof TableBodyOwnProps> {} + +/** @public */ +export type RowOwnProps = { + columns?: ReactAriaRowProps['columns']; + children?: ReactAriaRowProps['children']; + href?: string; + noTrack?: boolean; +}; + +/** @public */ +export interface RowProps + extends RowOwnProps, + Omit, keyof RowOwnProps> {} + +/** @public */ +export type ColumnOwnProps = { + children?: React.ReactNode; + className?: string; +}; + +/** @public */ +export interface ColumnProps + extends ColumnOwnProps, + Omit {} + +/** + * Own props for the {@link Cell} component. + * + * @public + */ +export type CellOwnProps = { + className?: string; +}; + +/** + * Props for the {@link Cell} component. + * + * `Cell` is a generic cell wrapper for custom cell content. When rendering + * cells via {@link ColumnConfig.cell} or a custom {@link RowRenderFn}, the + * returned element **must** be a cell component (`Cell`, `CellText`, or + * `CellProfile`) at the top level. Returning bare text or other elements + * without a cell wrapper will break the table layout. + * + * @public + */ +export interface CellProps + extends CellOwnProps, + Omit {} + +/** + * Own props for the {@link CellText} component. + * + * @public + */ +export type CellTextOwnProps = { title: string; description?: string; color?: TextColors; leadingIcon?: React.ReactNode | null; href?: string; -} + className?: string; +}; -/** @public */ -export interface CellProfileProps extends ReactAriaCellProps { +/** + * Props for the {@link CellText} component. + * + * `CellText` renders a table cell with a title and optional description. It + * is one of the cell components (`Cell`, `CellText`, `CellProfile`) that + * **must** be used as the top-level element returned from + * {@link ColumnConfig.cell} or a custom {@link RowRenderFn}. + * + * @public + */ +export interface CellTextProps + extends CellTextOwnProps, + Omit {} + +/** + * Own props for the {@link CellProfile} component. + * + * @public + */ +export type CellProfileOwnProps = { src?: string; name?: string; href?: string; description?: string; color?: TextColors; -} + className?: string; +}; -/** @public */ -export interface ColumnProps extends Omit { - children?: React.ReactNode; -} - -/** @public */ -export interface TableRootProps extends ReactAriaTableProps { - stale?: boolean; -} +/** + * Props for the {@link CellProfile} component. + * + * `CellProfile` renders a table cell with an avatar, name, and optional + * description. It is one of the cell components (`Cell`, `CellText`, + * `CellProfile`) that **must** be used as the top-level element returned + * from {@link ColumnConfig.cell} or a custom {@link RowRenderFn}. + * + * @public + */ +export interface CellProfileProps + extends CellProfileOwnProps, + Omit {} /** @public */ export interface TableItem { @@ -85,10 +191,27 @@ export interface PagePagination extends TablePaginationProps { /** @public */ export type TablePaginationType = NoPagination | PagePagination; -/** @public */ +/** + * Configuration for a single table column. + * + * @public + */ export interface ColumnConfig { id: string; label: string; + /** + * Renders the cell content for this column. + * + * **Important:** The returned element **must** be a cell component at the + * top level — either `Cell`, `CellText`, or `CellProfile`. Returning bare + * text, fragments, or other elements without a cell wrapper will break the + * table layout. + * + * @example + * ```tsx + * cell: item => + * ``` + */ cell: (item: T) => ReactElement; header?: () => ReactElement; isSortable?: boolean; @@ -107,7 +230,16 @@ export interface RowConfig { getIsDisabled?: (item: T) => boolean; } -/** @public */ +/** + * Custom render function for table rows. + * + * When using a custom row render function, each cell rendered inside the row + * **must** use a cell component (`Cell`, `CellText`, or `CellProfile`) as + * the top-level element. Returning bare text or other elements without a + * cell wrapper will break the table layout. + * + * @public + */ export type RowRenderFn = (params: { item: T; index: number; @@ -121,6 +253,12 @@ export interface TableSelection { onSelectionChange?: ReactAriaTableProps['onSelectionChange']; } +/** @public */ +export type VirtualizedProp = + | boolean + | { rowHeight: number } + | { estimatedRowHeight: number }; + /** @public */ export interface TableProps { columnConfig: readonly ColumnConfig[]; @@ -135,4 +273,5 @@ export interface TableProps { emptyState?: ReactNode; className?: string; style?: React.CSSProperties; + virtualized?: VirtualizedProp; } diff --git a/packages/ui/src/components/TablePagination/TablePagination.tsx b/packages/ui/src/components/TablePagination/TablePagination.tsx index 93ba1e0ef0..745088bf2f 100644 --- a/packages/ui/src/components/TablePagination/TablePagination.tsx +++ b/packages/ui/src/components/TablePagination/TablePagination.tsx @@ -14,27 +14,16 @@ * limitations under the License. */ -import clsx from 'clsx'; import { useId } from 'react-aria'; import { Text } from '../Text'; import { ButtonIcon } from '../ButtonIcon'; import { Select } from '../Select'; import type { TablePaginationProps, PageSizeOption } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { TablePaginationDefinition } from './definition'; -import styles from './TablePagination.module.css'; import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'; import { useMemo } from 'react'; -const DEFAULT_PAGE_SIZE_OPTIONS: PageSizeOption[] = [ - { label: 'Show 5 results', value: 5 }, - { label: 'Show 10 results', value: 10 }, - { label: 'Show 20 results', value: 20 }, - { label: 'Show 30 results', value: 30 }, - { label: 'Show 40 results', value: 40 }, - { label: 'Show 50 results', value: 50 }, -]; - function getOptionValue(option: number | PageSizeOption): number { return typeof option === 'number' ? option : option.value; } @@ -62,20 +51,23 @@ function normalizePageSizeOptions( * * @public */ -export function TablePagination({ - pageSize, - pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, - offset, - totalCount, - hasNextPage, - hasPreviousPage, - onNextPage, - onPreviousPage, - onPageSizeChange, - showPageSizeOptions = true, - getLabel, -}: TablePaginationProps) { - const { classNames } = useStyles(TablePaginationDefinition, {}); +export function TablePagination(props: TablePaginationProps) { + const { ownProps } = useDefinition(TablePaginationDefinition, props); + const { + classes, + pageSize, + pageSizeOptions, + offset, + totalCount, + hasNextPage, + hasPreviousPage, + onNextPage, + onPreviousPage, + onPageSizeChange, + showPageSizeOptions, + getLabel, + } = ownProps; + const labelId = useId(); const normalizedOptions = useMemo( () => normalizePageSizeOptions(pageSizeOptions), @@ -108,8 +100,8 @@ export function TablePagination({ } return ( -
    -
    +
    +
    {showPageSizeOptions && ( diff --git a/packages/ui/src/components/TextField/definition.ts b/packages/ui/src/components/TextField/definition.ts index c1cab8095d..3d9dcc88fc 100644 --- a/packages/ui/src/components/TextField/definition.ts +++ b/packages/ui/src/components/TextField/definition.ts @@ -14,13 +14,16 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { TextFieldOwnProps } from './types'; +import styles from './TextField.module.css'; /** * Component definition for TextField * @public */ -export const TextFieldDefinition = { +export const TextFieldDefinition = defineComponent()({ + styles, classNames: { root: 'bui-TextField', inputWrapper: 'bui-InputWrapper', @@ -28,9 +31,14 @@ export const TextFieldDefinition = { inputIcon: 'bui-InputIcon', inputAction: 'bui-InputAction', }, - dataAttributes: { - invalid: [true, false] as const, - disabled: [true, false] as const, - size: ['small', 'medium'] as const, + bg: 'consumer', + propDefs: { + size: { dataAttribute: true, default: 'small' }, + className: {}, + icon: {}, + placeholder: {}, + label: {}, + description: {}, + secondaryLabel: {}, }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index 456d6964a0..e65473d0c9 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -15,14 +15,39 @@ */ import type { TextFieldProps as AriaTextFieldProps } from 'react-aria-components'; -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; import type { Breakpoint } from '../../types'; import type { FieldLabelProps } from '../FieldLabel/types'; +/** @public */ +export type TextFieldOwnProps = { + /** + * The size of the text field + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | Partial>; + + className?: string; + + /** + * An icon to render before the input + */ + icon?: ReactNode; + + /** + * Text to display in the input when it has no value + */ + placeholder?: string; + + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + /** @public */ export interface TextFieldProps - extends AriaTextFieldProps, - Omit { + extends Omit, + TextFieldOwnProps { /** * The HTML input type for the text field * @@ -31,19 +56,4 @@ export interface TextFieldProps * search inputs and `PasswordField` for password inputs. */ type?: 'text' | 'email' | 'tel' | 'url'; - /** - * An icon to render before the input - */ - icon?: ReactNode; - - /** - * The size of the text field - * @defaultValue 'medium' - */ - size?: 'small' | 'medium' | Partial>; - - /** - * Text to display in the input when it has no value - */ - placeholder?: string; } diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx index a89bbe3862..876ff654b0 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.stories.tsx @@ -16,6 +16,7 @@ import preview from '../../../../../.storybook/preview'; import { ToggleButton } from './ToggleButton'; +import { Box } from '../Box'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { useState } from 'react'; @@ -65,21 +66,27 @@ export const Backgrounds = meta.story({ On Neutral 1 - + Toggle On Neutral 2 - - Toggle - + + + Toggle + + On Neutral 3 - - Toggle - + + + + Toggle + + + ), diff --git a/packages/ui/src/components/ToggleButton/ToggleButton.tsx b/packages/ui/src/components/ToggleButton/ToggleButton.tsx index d783681778..c827b2f88e 100644 --- a/packages/ui/src/components/ToggleButton/ToggleButton.tsx +++ b/packages/ui/src/components/ToggleButton/ToggleButton.tsx @@ -14,33 +14,27 @@ * limitations under the License. */ -import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; import { ToggleButton as AriaToggleButton } from 'react-aria-components'; import type { ToggleButtonProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { ToggleButtonDefinition } from './definition'; -import styles from './ToggleButton.module.css'; /** @public */ export const ToggleButton = forwardRef( (props: ToggleButtonProps, ref: Ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles( + const { ownProps, restProps, dataAttributes } = useDefinition( ToggleButtonDefinition, - { - size: 'small', - ...props, - }, + props, ); - - const { children, className, iconStart, iconEnd, ...rest } = cleanedProps; + const { classes, children, iconStart, iconEnd } = ownProps; return ( {renderProps => { // If children is a function, call it with render props; otherwise use children as-is @@ -48,10 +42,7 @@ export const ToggleButton = forwardRef( typeof children === 'function' ? children(renderProps) : children; return ( - + {iconStart} {renderedChildren} {iconEnd} diff --git a/packages/ui/src/components/ToggleButton/definition.ts b/packages/ui/src/components/ToggleButton/definition.ts index 98c22fef35..5137714417 100644 --- a/packages/ui/src/components/ToggleButton/definition.ts +++ b/packages/ui/src/components/ToggleButton/definition.ts @@ -14,18 +14,25 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { ToggleButtonOwnProps } from './types'; +import styles from './ToggleButton.module.css'; /** * Component definition for ToggleButton * @public */ -export const ToggleButtonDefinition = { +export const ToggleButtonDefinition = defineComponent()({ + styles, classNames: { root: 'bui-ToggleButton', content: 'bui-ToggleButtonContent', }, - dataAttributes: { - size: ['small', 'medium'] as const, + propDefs: { + size: { dataAttribute: true, default: 'small' }, + iconStart: {}, + iconEnd: {}, + children: {}, + className: {}, }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/ToggleButton/types.ts b/packages/ui/src/components/ToggleButton/types.ts index 2e5c5755f2..ba6569c2a2 100644 --- a/packages/ui/src/components/ToggleButton/types.ts +++ b/packages/ui/src/components/ToggleButton/types.ts @@ -18,13 +18,20 @@ import type { Breakpoint } from '../..'; import type { ReactElement } from 'react'; import type { ToggleButtonProps as AriaToggleButtonProps } from 'react-aria-components'; +/** @public */ +export type ToggleButtonOwnProps = { + size?: 'small' | 'medium' | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: AriaToggleButtonProps['children']; + className?: string; +}; + /** * Properties for {@link ToggleButton} * * @public */ -export interface ToggleButtonProps extends AriaToggleButtonProps { - size?: 'small' | 'medium' | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; -} +export interface ToggleButtonProps + extends Omit, + ToggleButtonOwnProps {} diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css index fef9b33a9a..827b7f18d6 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css @@ -102,7 +102,7 @@ } /* Focus ring on group surface */ - .bui-ToggleButtonGroup:focus-visible { + .bui-ToggleButtonGroup[data-focus-visible] { outline: 2px solid var(--bui-ring); outline-offset: 2px; } diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx index 617cd0c0fa..c98a2e18f1 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx @@ -17,6 +17,7 @@ import preview from '../../../../../.storybook/preview'; import { ToggleButtonGroup } from './ToggleButtonGroup'; import { ToggleButton } from '../ToggleButton/ToggleButton'; +import { Box } from '../Box'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { useState } from 'react'; @@ -100,7 +101,7 @@ export const Backgrounds = meta.story({ On Neutral 1 - + On Neutral 2 - - - Option 1 - Option 2 - Option 3 - - + + + + Option 1 + Option 2 + Option 3 + + + On Neutral 3 - - - Option 1 - Option 2 - Option 3 - - + + + + + Option 1 + Option 2 + Option 3 + + + + ), diff --git a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 43449f8472..61c715b59d 100644 --- a/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -14,33 +14,23 @@ * limitations under the License. */ -import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; import { ToggleButtonGroup as AriaToggleButtonGroup } from 'react-aria-components'; import type { ToggleButtonGroupProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { ToggleButtonGroupDefinition } from './definition'; -import styles from './ToggleButtonGroup.module.css'; /** @public */ export const ToggleButtonGroup = forwardRef( (props: ToggleButtonGroupProps, ref: Ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles( + const { ownProps, restProps } = useDefinition( ToggleButtonGroupDefinition, - { - ...props, - }, + props, ); - - const { className, children, ...rest } = cleanedProps; + const { classes, children } = ownProps; return ( - + {children} ); diff --git a/packages/ui/src/components/ToggleButtonGroup/definition.ts b/packages/ui/src/components/ToggleButtonGroup/definition.ts index 7acf6828f0..9e31b8d6cc 100644 --- a/packages/ui/src/components/ToggleButtonGroup/definition.ts +++ b/packages/ui/src/components/ToggleButtonGroup/definition.ts @@ -14,17 +14,22 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { ToggleButtonGroupOwnProps } from './types'; +import styles from './ToggleButtonGroup.module.css'; /** * Component definition for ToggleButtonGroup * @public */ -export const ToggleButtonGroupDefinition = { - classNames: { - root: 'bui-ToggleButtonGroup', - }, - dataAttributes: { - orientation: ['horizontal', 'vertical'] as const, - }, -} as const satisfies ComponentDefinition; +export const ToggleButtonGroupDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-ToggleButtonGroup', + }, + propDefs: { + className: {}, + children: {}, + }, + }); diff --git a/packages/ui/src/components/ToggleButtonGroup/types.ts b/packages/ui/src/components/ToggleButtonGroup/types.ts index 7a57708750..b2bdb1e831 100644 --- a/packages/ui/src/components/ToggleButtonGroup/types.ts +++ b/packages/ui/src/components/ToggleButtonGroup/types.ts @@ -14,10 +14,16 @@ * limitations under the License. */ +import type { ReactNode } from 'react'; import type { ToggleButtonGroupProps as AriaToggleButtonGroupProps } from 'react-aria-components'; +/** @public */ +export type ToggleButtonGroupOwnProps = { + className?: string; + children?: ReactNode; +}; + /** @public */ export interface ToggleButtonGroupProps - extends Omit { - orientation?: NonNullable; -} + extends Omit, + ToggleButtonGroupOwnProps {} diff --git a/packages/ui/src/components/Tooltip/Tooltip.module.css b/packages/ui/src/components/Tooltip/Tooltip.module.css index 22a3283790..51ef56eaaf 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.module.css +++ b/packages/ui/src/components/Tooltip/Tooltip.module.css @@ -18,13 +18,13 @@ @layer components { .bui-Tooltip { + --tooltip-border-radius: 4px; box-shadow: var(--bui-shadow); - border-radius: 4px; - background: var(--bui-bg-popover); + border-radius: var(--tooltip-border-radius); + background: var(--bui-bg-app); border: 1px solid var(--bui-border-1); forced-color-adjust: none; outline: none; - padding: var(--bui-space-2) var(--bui-space-3); max-width: 240px; /* fixes FF gap */ transform: translate3d(0, 0, 0); @@ -62,6 +62,11 @@ } } + .bui-TooltipContent { + padding: var(--bui-space-2) var(--bui-space-3); + border-radius: var(--tooltip-border-radius); + } + .bui-TooltipArrow { & svg { display: block; @@ -73,11 +78,14 @@ we split the stroke and fill across separate elements in order to guarantee that the stroke is always overlaying a consistent color. */ - path:nth-child(1) { - fill: var(--bui-bg-popover); + use:nth-of-type(1) { + fill: var(--bui-bg-app); + } + use:nth-of-type(2) { + fill: var(--bui-bg-neutral-1); } - path:nth-child(2) { + path { fill: var(--bui-border-1); } diff --git a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx index a636a356ef..c10d63f315 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx @@ -55,6 +55,24 @@ export const Default = meta.story({ }); export const IsOpen = meta.story({ + parameters: { layout: 'fullscreen' }, + decorators: [ + Story => ( +
    + +
    + ), + ], args: { ...Default.input.args, isOpen: true, diff --git a/packages/ui/src/components/Tooltip/Tooltip.tsx b/packages/ui/src/components/Tooltip/Tooltip.tsx index 9099f1eef2..1d88fd0b41 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/ui/src/components/Tooltip/Tooltip.tsx @@ -15,17 +15,18 @@ */ import { forwardRef } from 'react'; +import { useId } from 'react-aria'; import { OverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger, TooltipTriggerComponentProps, } from 'react-aria-components'; -import clsx from 'clsx'; -import { TooltipProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import type { TooltipProps } from './types'; +import { useDefinition } from '../../hooks/useDefinition'; import { TooltipDefinition } from './definition'; -import styles from './Tooltip.module.css'; +import { Box } from '../Box'; +import { BgReset } from '../../hooks/useBg'; /** @public */ export const TooltipTrigger = (props: TooltipTriggerComponentProps) => { @@ -37,28 +38,33 @@ export const TooltipTrigger = (props: TooltipTriggerComponentProps) => { /** @public */ export const Tooltip = forwardRef( (props, ref) => { - const { classNames, cleanedProps } = useStyles(TooltipDefinition, props); - const { className, children, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(TooltipDefinition, props); + const { classes, children } = ownProps; + const svgPathId = useId(); return ( - - + + - + + + + + + + - {children} + + + {children} + + ); }, diff --git a/packages/ui/src/components/Tooltip/definition.ts b/packages/ui/src/components/Tooltip/definition.ts index a5b3d61ea8..226e4cef9d 100644 --- a/packages/ui/src/components/Tooltip/definition.ts +++ b/packages/ui/src/components/Tooltip/definition.ts @@ -14,15 +14,23 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { TooltipOwnProps } from './types'; +import styles from './Tooltip.module.css'; /** * Component definition for Tooltip * @public */ -export const TooltipDefinition = { +export const TooltipDefinition = defineComponent()({ + styles, classNames: { tooltip: 'bui-Tooltip', + content: 'bui-TooltipContent', arrow: 'bui-TooltipArrow', }, -} as const satisfies ComponentDefinition; + propDefs: { + children: {}, + className: {}, + }, +}); diff --git a/packages/ui/src/components/Tooltip/index.ts b/packages/ui/src/components/Tooltip/index.ts index 43bd385c65..b70bce61b8 100644 --- a/packages/ui/src/components/Tooltip/index.ts +++ b/packages/ui/src/components/Tooltip/index.ts @@ -15,5 +15,5 @@ */ export { TooltipTrigger, Tooltip } from './Tooltip'; -export type { TooltipProps } from './types'; +export type { TooltipOwnProps, TooltipProps } from './types'; export { TooltipDefinition } from './definition'; diff --git a/packages/ui/src/components/Tooltip/types.ts b/packages/ui/src/components/Tooltip/types.ts index 2ef44fbbf4..d395ce9004 100644 --- a/packages/ui/src/components/Tooltip/types.ts +++ b/packages/ui/src/components/Tooltip/types.ts @@ -14,9 +14,15 @@ * limitations under the License. */ -import { TooltipProps as AriaTooltipProps } from 'react-aria-components'; +import type { TooltipProps as AriaTooltipProps } from 'react-aria-components'; /** @public */ -export interface TooltipProps extends Omit { +export type TooltipOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +/** @public */ +export interface TooltipProps + extends Omit, + TooltipOwnProps {} diff --git a/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx index cfc796ef9e..ea20d2f6cd 100644 --- a/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx +++ b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx @@ -14,11 +14,9 @@ * limitations under the License. */ -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { VisuallyHiddenDefinition } from './definition'; import { VisuallyHiddenProps } from './types'; -import styles from './VisuallyHidden.module.css'; -import clsx from 'clsx'; /** * Visually hides content while keeping it accessible to screen readers. @@ -30,16 +28,11 @@ import clsx from 'clsx'; * @public */ export const VisuallyHidden = (props: VisuallyHiddenProps) => { - const { classNames, cleanedProps } = useStyles( + const { ownProps, restProps } = useDefinition( VisuallyHiddenDefinition, props, ); - const { className, ...rest } = cleanedProps; + const { classes } = ownProps; - return ( -
    - ); + return
    ; }; diff --git a/packages/ui/src/components/VisuallyHidden/definition.ts b/packages/ui/src/components/VisuallyHidden/definition.ts index 5be52f89ec..c7aaf0a6da 100644 --- a/packages/ui/src/components/VisuallyHidden/definition.ts +++ b/packages/ui/src/components/VisuallyHidden/definition.ts @@ -14,14 +14,21 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { VisuallyHiddenOwnProps } from './types'; +import styles from './VisuallyHidden.module.css'; /** * Component definition for VisuallyHidden * @public */ -export const VisuallyHiddenDefinition = { - classNames: { - root: 'bui-VisuallyHidden', - }, -} as const satisfies ComponentDefinition; +export const VisuallyHiddenDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-VisuallyHidden', + }, + propDefs: { + className: {}, + }, + }); diff --git a/packages/ui/src/components/VisuallyHidden/index.ts b/packages/ui/src/components/VisuallyHidden/index.ts index c3668df403..3f036c7c8c 100644 --- a/packages/ui/src/components/VisuallyHidden/index.ts +++ b/packages/ui/src/components/VisuallyHidden/index.ts @@ -15,5 +15,5 @@ */ export { VisuallyHidden } from './VisuallyHidden'; -export type { VisuallyHiddenProps } from './types'; +export type { VisuallyHiddenOwnProps, VisuallyHiddenProps } from './types'; export { VisuallyHiddenDefinition } from './definition'; diff --git a/packages/ui/src/components/VisuallyHidden/types.ts b/packages/ui/src/components/VisuallyHidden/types.ts index 88951d7ee2..d9c5700780 100644 --- a/packages/ui/src/components/VisuallyHidden/types.ts +++ b/packages/ui/src/components/VisuallyHidden/types.ts @@ -14,13 +14,18 @@ * limitations under the License. */ -import { ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; + +/** @public */ +export type VisuallyHiddenOwnProps = { + className?: string; +}; /** * Properties for {@link VisuallyHidden} * * @public */ -export interface VisuallyHiddenProps extends ComponentProps<'div'> { - children?: React.ReactNode; -} +export interface VisuallyHiddenProps + extends Omit, 'className'>, + VisuallyHiddenOwnProps {} diff --git a/packages/ui/src/css/tokens.css b/packages/ui/src/css/tokens.css index 159ebfef9e..9426993ea5 100644 --- a/packages/ui/src/css/tokens.css +++ b/packages/ui/src/css/tokens.css @@ -79,11 +79,10 @@ /* Neutral background colors */ --bui-bg-app: #f8f8f8; - --bui-bg-popover: #ffffff; --bui-bg-neutral-1: #fff; - --bui-bg-neutral-1-hover: oklch(0% 0 0 / 12%); - --bui-bg-neutral-1-pressed: oklch(0% 0 0 / 16%); + --bui-bg-neutral-1-hover: oklch(0% 0 0 / 6%); + --bui-bg-neutral-1-pressed: oklch(0% 0 0 / 12%); --bui-bg-neutral-1-disabled: oklch(0% 0 0 / 6%); --bui-bg-neutral-2: oklch(0% 0 0 / 6%); @@ -154,7 +153,6 @@ /* Neutral background colors */ --bui-bg-app: #333333; - --bui-bg-popover: #1a1a1a; --bui-bg-neutral-1: oklch(100% 0 0 / 10%); --bui-bg-neutral-1-hover: oklch(100% 0 0 / 14%); diff --git a/packages/ui/src/definitions.ts b/packages/ui/src/definitions.ts index 59003e8070..5aeac8de5a 100644 --- a/packages/ui/src/definitions.ts +++ b/packages/ui/src/definitions.ts @@ -43,12 +43,23 @@ export { GridItemDefinition, } from './components/Grid/definition'; export { PluginHeaderDefinition } from './components/PluginHeader/definition'; -export { HeaderPageDefinition } from './components/HeaderPage/definition'; +export { + HeaderDefinition, + HeaderPageDefinition, +} from './components/Header/definition'; export { LinkDefinition } from './components/Link/definition'; +export { + ListDefinition, + ListRowDefinition, +} from './components/List/definition'; export { MenuDefinition } from './components/Menu/definition'; export { PasswordFieldDefinition } from './components/PasswordField/definition'; export { PopoverDefinition } from './components/Popover/definition'; export { RadioGroupDefinition } from './components/RadioGroup/definition'; +export { + SearchAutocompleteDefinition, + SearchAutocompleteItemDefinition, +} from './components/SearchAutocomplete/definition'; export { SearchFieldDefinition } from './components/SearchField/definition'; export { SelectDefinition } from './components/Select/definition'; export { SkeletonDefinition } from './components/Skeleton/definition'; diff --git a/packages/ui/src/guidelines/CardsWithTable.stories.tsx b/packages/ui/src/guidelines/CardsWithTable.stories.tsx new file mode 100644 index 0000000000..23e5923327 --- /dev/null +++ b/packages/ui/src/guidelines/CardsWithTable.stories.tsx @@ -0,0 +1,282 @@ +/* + * 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. + */ + +/* eslint-disable no-restricted-syntax */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import { BUIProvider } from '../provider'; +import { + Card, + CardHeader, + CardBody, + Container, + Grid, + Flex, + Text, + Table, + CellText, + CellProfile, + useTable, + type ColumnConfig, + PluginHeader, + Header, + Button, +} from '..'; + +// --------------------------------------------------------------------------- +// Metric card data +// --------------------------------------------------------------------------- + +interface MetricCard { + label: string; + value: string; + trend: string; + trendColor: 'primary' | 'secondary' | 'success' | 'danger' | 'warning'; +} + +const metrics: MetricCard[] = [ + { + label: 'Total Components', + value: '142', + trend: '+12 this week', + trendColor: 'success', + }, + { + label: 'Active Services', + value: '58', + trend: '94% healthy', + trendColor: 'success', + }, + { + label: 'Recent Deployments', + value: '23', + trend: 'in the last 24 h', + trendColor: 'secondary', + }, +]; + +// --------------------------------------------------------------------------- +// Table data +// --------------------------------------------------------------------------- + +interface ServiceItem { + id: string; + name: string; + owner: string; + ownerAvatar: string; + type: 'service' | 'library' | 'website' | 'documentation'; + lifecycle: 'production' | 'experimental'; + description: string; +} + +const services: ServiceItem[] = [ + { + id: '1', + name: 'authentication-service', + owner: 'security-team', + ownerAvatar: 'https://github.com/identicons/security-team.png', + type: 'service', + lifecycle: 'production', + description: 'Handles user authentication, sessions and token refresh.', + }, + { + id: '2', + name: 'api-gateway', + owner: 'platform-team', + ownerAvatar: 'https://github.com/identicons/platform-team.png', + type: 'service', + lifecycle: 'production', + description: 'Routes and validates all inbound API requests.', + }, + { + id: '3', + name: 'frontend-core', + owner: 'design-system', + ownerAvatar: 'https://github.com/identicons/design-system.png', + type: 'library', + lifecycle: 'production', + description: 'Shared UI components and design tokens.', + }, + { + id: '4', + name: 'data-pipeline', + owner: 'data-team', + ownerAvatar: 'https://github.com/identicons/data-team.png', + type: 'service', + lifecycle: 'experimental', + description: 'Streaming data ingestion pipeline for analytics.', + }, + { + id: '5', + name: 'developer-portal', + owner: 'devex-team', + ownerAvatar: 'https://github.com/identicons/devex-team.png', + type: 'website', + lifecycle: 'production', + description: 'Internal developer portal built on Backstage.', + }, + { + id: '6', + name: 'notification-service', + owner: 'platform-team', + ownerAvatar: 'https://github.com/identicons/platform-team.png', + type: 'service', + lifecycle: 'production', + description: 'Sends emails, Slack messages and push notifications.', + }, + { + id: '7', + name: 'search-indexer', + owner: 'search-team', + ownerAvatar: 'https://github.com/identicons/search-team.png', + type: 'service', + lifecycle: 'experimental', + description: 'Indexes catalog entities for full-text search.', + }, + { + id: '8', + name: 'billing-service', + owner: 'payments-team', + ownerAvatar: 'https://github.com/identicons/payments-team.png', + type: 'service', + lifecycle: 'production', + description: 'Manages subscriptions, invoices and payment processing.', + }, +]; + +// --------------------------------------------------------------------------- +// Stat card component +// --------------------------------------------------------------------------- + +const StatCard = ({ label, value, trend, trendColor }: MetricCard) => ( + + {label} + + + + {value} + + + {trend} + + + + +); + +// --------------------------------------------------------------------------- +// Page layout component (the actual story render) +// --------------------------------------------------------------------------- + +const columns: ColumnConfig[] = [ + { + id: 'name', + label: 'Name', + isRowHeader: true, + defaultWidth: '3fr', + cell: item => ( + + ), + }, + { + id: 'owner', + label: 'Owner', + defaultWidth: '2fr', + cell: item => , + }, + { + id: 'type', + label: 'Type', + defaultWidth: '1fr', + cell: item => , + }, + { + id: 'lifecycle', + label: 'Lifecycle', + defaultWidth: '1fr', + cell: item => ( + + ), + }, +]; + +const CardsWithTableLayout = () => { + const { tableProps } = useTable({ + mode: 'complete', + getData: () => services, + paginationOptions: { pageSize: 5 }, + }); + + return ( + <> + +
    Custom action} + /> + + + + {metrics.map(metric => ( + + ))} + +
    + + + + ); +}; + +// --------------------------------------------------------------------------- +// Storybook meta +// --------------------------------------------------------------------------- + +const meta = { + title: 'Guidelines/Cards with Table', + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (Story: () => JSX.Element) => ( + + + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/packages/ui/src/hooks/useBg.tsx b/packages/ui/src/hooks/useBg.tsx index 6e298fedf9..2284e5f337 100644 --- a/packages/ui/src/hooks/useBg.tsx +++ b/packages/ui/src/hooks/useBg.tsx @@ -69,6 +69,24 @@ export const BgProvider = ({ bg, children }: BgProviderProps) => { ); }; +/** + * Resets the bg context to undefined, cutting any inherited neutral chain. + * Use this inside overlay components (Popover, Tooltip, Dialog, Menu) so + * their content always starts from neutral-1 regardless of where the trigger + * is placed in the tree. + * + * @internal + */ +export const BgReset = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; + /** * Hook for consumer components (e.g. Button) to read the parent bg context. * @@ -90,10 +108,14 @@ export function useBgConsumer(): BgContextValue { * * - `bg` is `undefined` -- transparent, no context change, returns `{ bg: undefined }`. * This is the default for Box, Flex, and Grid (they do **not** auto-increment). - * - `bg` is a `ContainerBg` value -- uses that value directly (e.g. `'neutral-1'`). - * - `bg` is `'neutral-auto'` -- increments the neutral level from the parent context, - * capping at `neutral-3`. Only components that explicitly pass `'neutral-auto'` - * (e.g. Card) will auto-increment; it is never implicit. + * - `bg` is `'neutral'` -- when the parent bg is neutral, increments the neutral + * level from the parent context, capping at `neutral-3`. When the parent bg is + * an intent (`'danger'` | `'warning'` | `'success'`), the intent passes through + * unchanged (i.e. `bg: 'neutral'` does not override the parent intent). The + * increment is always relative to the parent; it is not possible to pin a + * container to an explicit neutral level. + * - `bg` is `'danger'` | `'warning'` | `'success'` -- sets the bg to that intent + * explicitly, regardless of the parent value. * * **Capping:** * @@ -116,7 +138,7 @@ export function useBgProvider(bg?: Responsive): BgContextValue { const resolved = resolveResponsiveValue(bg, breakpoint); - if (resolved === 'neutral-auto') { + if (resolved === 'neutral') { return { bg: incrementNeutralBg(context.bg) }; } diff --git a/packages/ui/src/hooks/useBreakpoint.ts b/packages/ui/src/hooks/useBreakpoint.ts index ac2d0567be..bd37eec4c4 100644 --- a/packages/ui/src/hooks/useBreakpoint.ts +++ b/packages/ui/src/hooks/useBreakpoint.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useMediaQuery } from './useMediaQuery'; +import { useMemo } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import type { Breakpoint } from '../types'; export const breakpoints: { name: string; id: Breakpoint; value: number }[] = [ @@ -25,40 +26,95 @@ export const breakpoints: { name: string; id: Breakpoint; value: number }[] = [ { name: 'Extra Large', id: 'xl', value: 1536 }, ]; -/** @public */ -export const useBreakpoint = () => { - // Call all media queries at the top level - const matches = breakpoints.map(breakpoint => { - return useMediaQuery(`(min-width: ${breakpoint.value}px)`); - }); +const breakpointIndex = new Map( + breakpoints.map((bp, i) => [bp.id, i]), +); - // Pre-calculate all the up/down values we need - const upMatches = new Map( - breakpoints.map(bp => [bp.id, useMediaQuery(`(min-width: ${bp.value}px)`)]), - ); +function bpIndex(key: Breakpoint): number { + return breakpointIndex.get(key) ?? 0; +} - const downMatches = new Map( - breakpoints.map(bp => [ - bp.id, - useMediaQuery(`(max-width: ${bp.value - 1}px)`), - ]), - ); +const IS_SERVER = + typeof window === 'undefined' || typeof window.matchMedia === 'undefined'; - let breakpoint: Breakpoint = breakpoints[0].id; - for (let i = matches.length - 1; i >= 0; i--) { - if (matches[i]) { - breakpoint = breakpoints[i].id; - break; +function computeBreakpoint(): Breakpoint { + if (IS_SERVER) { + return 'initial'; + } + for (let i = breakpoints.length - 1; i >= 0; i--) { + if (window.matchMedia(`(min-width: ${breakpoints[i].value}px)`).matches) { + return breakpoints[i].id; } } + return 'initial'; +} - return { - breakpoint, - up: (key: Breakpoint): boolean => { - return upMatches.get(key) ?? false; - }, - down: (key: Breakpoint): boolean => { - return downMatches.get(key) ?? false; - }, +// --- Module-scoped singleton store --- +// This is intentionally not a global singleton. Multiple copies of this module +// (e.g. different package versions or module federation remotes) each get their +// own store. This avoids cross-version coupling at the cost of a few extra +// listeners, which is a fine trade-off. +// `current` is initialized lazily on first client-side access. + +let current: Breakpoint | undefined; +const listeners = new Set<() => void>(); +let initialized = false; + +function ensureInitialized(): void { + if (initialized || IS_SERVER) { + return; + } + initialized = true; + current = computeBreakpoint(); + + // Register one listener per breakpoint. When any query fires, re-evaluate + // all breakpoints to find the new active one. Notify subscribers only if + // the active breakpoint actually changed. + for (const bp of breakpoints) { + const mql = window.matchMedia(`(min-width: ${bp.value}px)`); + mql.addEventListener('change', () => { + const next = computeBreakpoint(); + if (next !== current) { + current = next; + for (const cb of listeners) { + cb(); + } + } + }); + } +} + +function subscribe(callback: () => void): () => void { + ensureInitialized(); + listeners.add(callback); + return () => { + listeners.delete(callback); }; +} + +function getSnapshot(): Breakpoint { + ensureInitialized(); + return current ?? 'initial'; +} + +function getServerSnapshot(): Breakpoint { + return 'initial'; +} + +/** @public */ +export const useBreakpoint = () => { + const breakpoint = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot, + ); + + return useMemo( + () => ({ + breakpoint, + up: (key: Breakpoint): boolean => bpIndex(breakpoint) >= bpIndex(key), + down: (key: Breakpoint): boolean => bpIndex(breakpoint) < bpIndex(key), + }), + [breakpoint], + ); }; diff --git a/packages/ui/src/hooks/useDefinition/defineComponent.ts b/packages/ui/src/hooks/useDefinition/defineComponent.ts index 6dbfb5742b..fc58a6774e 100644 --- a/packages/ui/src/hooks/useDefinition/defineComponent.ts +++ b/packages/ui/src/hooks/useDefinition/defineComponent.ts @@ -14,13 +14,19 @@ * limitations under the License. */ -import type { ComponentConfig, BgPropsConstraint } from './types'; +import type { + ComponentConfig, + BgPropsConstraint, + AnalyticsPropsConstraint, +} from './types'; export function defineComponent

    >() { return < const S extends Record, const C extends ComponentConfig, >( - config: C & BgPropsConstraint, + config: C & + BgPropsConstraint & + AnalyticsPropsConstraint, ): C => config; } diff --git a/packages/ui/src/hooks/useDefinition/helpers.ts b/packages/ui/src/hooks/useDefinition/helpers.ts index 3baf07b5af..8a990b4b6a 100644 --- a/packages/ui/src/hooks/useDefinition/helpers.ts +++ b/packages/ui/src/hooks/useDefinition/helpers.ts @@ -16,7 +16,7 @@ import { breakpoints } from '../useBreakpoint'; import { utilityClassMap } from '../../utils/utilityClassMap'; -import type { UnwrapResponsive, UtilityStyle } from './types'; +import type { ComponentConfig, UnwrapResponsive, UtilityStyle } from './types'; const namedBreakpoints = breakpoints.filter(b => b.id !== 'initial'); @@ -57,6 +57,42 @@ export function resolveResponsiveValue( return value as UnwrapResponsive; } +export function resolveDefinitionProps>( + definition: D, + props: Record, + breakpoint: string, +): { + ownPropsResolved: Record; + restProps: Record; +} { + const ownPropKeys = new Set(Object.keys(definition.propDefs)); + const utilityPropKeys = new Set(definition.utilityProps ?? []); + + const ownPropsRaw: Record = {}; + const restProps: Record = {}; + + for (const [key, value] of Object.entries(props)) { + if (ownPropKeys.has(key)) { + ownPropsRaw[key] = value; + } else if (!(utilityPropKeys as Set).has(key)) { + restProps[key] = value; + } + } + + const ownPropsResolved: Record = {}; + + for (const [key, config] of Object.entries(definition.propDefs)) { + const rawValue = ownPropsRaw[key]; + const resolvedValue = resolveResponsiveValue(rawValue, breakpoint); + const finalValue = resolvedValue ?? (config as any).default; + if (finalValue !== undefined) { + ownPropsResolved[key] = finalValue; + } + } + + return { ownPropsResolved, restProps }; +} + export function processUtilityProps( props: Record, utilityPropKeys: readonly Keys[], diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index 45ce5e3dc8..a819cb5d35 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -16,6 +16,7 @@ import type { ReactNode } from 'react'; import type { Responsive } from '../../types'; +import type { AnalyticsTracker } from '../../analytics/types'; import type { utilityClassMap } from '../../utils/utilityClassMap'; export type UnwrapResponsive = T extends Responsive ? U : T; @@ -43,18 +44,45 @@ export interface ComponentConfig< * - `'consumer'` — calls `useBgConsumer`, sets `data-on-bg` */ bg?: 'provider' | 'consumer'; + /** + * Whether this component fires analytics events. + * When true, `useDefinition` will call `useAnalytics()` and return + * an `analytics` tracker. The component's own props type must include + * `noTrack?: boolean`. + */ + analytics?: boolean; } /** * Type constraint that validates bg props are present in the props type. - * - Provider components must include 'bg' in their props + * - Provider components must include 'bg' in their props and 'children' in propDefs * - Consumer components don't need a bg prop */ export type BgPropsConstraint = Bg extends 'provider' ? 'bg' extends keyof P + ? 'children' extends keyof P + ? {} extends Pick + ? { + __error: 'Bg provider components cannot have children as optional.'; + } + : {} + : { + __error: 'Bg provider components must include children in own props type.'; + } + : { + __error: 'Bg provider components must include bg in own props type.'; + } + : {}; + +/** + * Type constraint that validates analytics props are present in the props type. + * Components with analytics: true must include 'noTrack' in their props. + */ +export type AnalyticsPropsConstraint = Analytics extends true + ? 'noTrack' extends keyof P ? {} : { - __error: 'Bg provider components must include bg in props type.'; + __error: 'Analytics components must include noTrack in own props type.'; } : {}; @@ -127,10 +155,10 @@ type ResolvedUtilityStyle> = UtilityStyle< UtilityKeys >; -export interface UseDefinitionResult< +export type UseDefinitionResult< D extends ComponentConfig, P extends Record, -> { +> = { ownProps: ResolveBgProps>; // Rest props excludes both propDefs keys AND utility prop keys @@ -141,4 +169,4 @@ export interface UseDefinitionResult< dataAttributes: DataAttributes; utilityStyle: ResolvedUtilityStyle; -} +} & (D['analytics'] extends true ? { analytics: AnalyticsTracker } : {}); diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index d1fe9af96c..20ce94e123 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -18,7 +18,9 @@ import { ReactNode } from 'react'; import clsx from 'clsx'; import { useBreakpoint } from '../useBreakpoint'; import { useBgProvider, useBgConsumer, BgProvider } from '../useBg'; -import { resolveResponsiveValue, processUtilityProps } from './helpers'; +import { resolveDefinitionProps, processUtilityProps } from './helpers'; +import { useAnalytics } from '../../analytics/useAnalytics'; +import { noopTracker } from '../../analytics/useAnalytics'; import type { ComponentConfig, UseDefinitionOptions, @@ -36,39 +38,19 @@ export function useDefinition< ): UseDefinitionResult { const { breakpoint } = useBreakpoint(); - // Provider: resolve bg and provide context for children - const providerBg = useBgProvider( - definition.bg === 'provider' ? props.bg : undefined, + // Resolve all props centrally — applies responsive values and defaults + const { ownPropsResolved, restProps } = resolveDefinitionProps( + definition, + props, + breakpoint, ); - // Consumer: read parent context bg - const consumerBg = useBgConsumer(); - - const ownPropKeys = new Set(Object.keys(definition.propDefs)); - const utilityPropKeys = new Set(definition.utilityProps ?? []); - - const ownPropsRaw: Record = {}; - const restProps: Record = {}; - - for (const [key, value] of Object.entries(props)) { - if (ownPropKeys.has(key)) { - ownPropsRaw[key] = value; - } else if (!(utilityPropKeys as Set).has(key)) { - restProps[key] = value; - } - } - - const ownPropsResolved: Record = {}; const dataAttributes: Record = {}; for (const [key, config] of Object.entries(definition.propDefs)) { - const rawValue = ownPropsRaw[key]; - const resolvedValue = resolveResponsiveValue(rawValue, breakpoint); - const finalValue = resolvedValue ?? (config as any).default; + const finalValue = ownPropsResolved[key]; if (finalValue !== undefined) { - ownPropsResolved[key] = finalValue; - // Skip data-bg for bg prop when the provider path handles it if (key === 'bg' && definition.bg === 'provider') continue; @@ -79,6 +61,14 @@ export function useDefinition< } } + // Provider: resolve bg and provide context for children + const providerBg = useBgProvider( + definition.bg === 'provider' ? ownPropsResolved.bg : undefined, + ); + + // Consumer: read parent context bg + const consumerBg = useBgConsumer(); + // Provider: set data-bg from the resolved provider bg if (definition.bg === 'provider' && providerBg.bg !== undefined) { dataAttributes['data-bg'] = String(providerBg.bg); @@ -94,6 +84,14 @@ export function useDefinition< (definition.utilityProps ?? []) as readonly UtilityKeys[], ); + // Analytics: conditionally call useAnalytics based on definition flag + // Safe: definition is a module-level constant, condition never changes at runtime + let analytics = noopTracker; + if (definition.analytics) { + const tracker = useAnalytics(); + analytics = ownPropsResolved.noTrack ? noopTracker : tracker; + } + const utilityTarget = options?.utilityTarget ?? 'root'; const classNameTarget = options?.classNameTarget ?? 'root'; @@ -132,5 +130,6 @@ export function useDefinition< restProps, dataAttributes, utilityStyle, + ...(definition.analytics ? { analytics } : {}), } as unknown as UseDefinitionResult; } diff --git a/packages/ui/src/hooks/useStyles.ts b/packages/ui/src/hooks/useStyles.ts deleted file mode 100644 index 7bb25be651..0000000000 --- a/packages/ui/src/hooks/useStyles.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 { useBreakpoint } from './useBreakpoint'; -import type { ComponentDefinition } from '../types'; -import { - resolveResponsiveValue, - processUtilityProps, -} from './useDefinition/helpers'; - -/** - * React hook to get class names and data attributes for a component with responsive support - * @param componentDefinition - The component's definition object - * @param props - All component props - * @returns Object with classNames, dataAttributes, utilityClasses, style, and cleanedProps - */ -export function useStyles< - T extends ComponentDefinition, - P extends Record = Record, ->( - componentDefinition: T, - props: P = {} as P, -): { - classNames: T['classNames']; - dataAttributes: Record; - utilityClasses: string; - style: React.CSSProperties; - cleanedProps: P; -} { - const { breakpoint } = useBreakpoint(); - const classNames = componentDefinition.classNames; - const utilityPropNames = - ('utilityProps' in componentDefinition - ? componentDefinition.utilityProps - : []) || []; - - // Extract data attribute names from component definition - const dataAttributeNames = - 'dataAttributes' in componentDefinition - ? Object.keys(componentDefinition.dataAttributes || {}) - : []; - - // Extract existing style from props - const incomingStyle = props.style || {}; - - // Generate data attributes from component definition - const dataAttributes: Record = {}; - for (const key of dataAttributeNames) { - const value = props[key]; - if (value !== undefined && value !== null) { - // Handle boolean and number values directly - if (typeof value === 'boolean' || typeof value === 'number') { - dataAttributes[`data-${key}`] = String(value); - } else { - const resolvedValue = resolveResponsiveValue(value, breakpoint); - if (resolvedValue !== undefined) { - dataAttributes[`data-${key}`] = resolvedValue; - } - } - } - } - - // Generate utility classes and custom styles from component's allowed utility props - const { utilityClasses, utilityStyle: generatedStyle } = processUtilityProps( - props, - utilityPropNames, - ); - - // Create cleaned props by excluding only utility props - // All other props (including data attributes, style, children, etc.) remain - const utilityPropsSet = new Set(utilityPropNames); - - const cleanedPropsBase = Object.keys(props).reduce((acc, key) => { - if (!utilityPropsSet.has(key)) { - acc[key] = props[key]; - } - return acc; - }, {} as any); - - // Merge incoming style with generated styles (incoming styles take precedence) - const mergedStyle = { - ...generatedStyle, - ...incomingStyle, - }; - - // Add merged style to cleanedProps - const cleanedProps = { - ...cleanedPropsBase, - style: mergedStyle, - } as P; - - return { - classNames, - dataAttributes, - utilityClasses, - style: mergedStyle, - cleanedProps, - }; -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index c70aa9d325..4cb1d323c7 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -36,7 +36,7 @@ export * from './components/Card'; export * from './components/Dialog'; export * from './components/FieldLabel'; export * from './components/PluginHeader'; -export * from './components/HeaderPage'; +export * from './components/Header'; export * from './components/ButtonIcon'; export * from './components/ButtonLink'; export * from './components/Checkbox'; @@ -51,8 +51,10 @@ export * from './components/PasswordField'; export * from './components/Tooltip'; export * from './components/Menu'; export * from './components/Popover'; +export * from './components/SearchAutocomplete'; export * from './components/SearchField'; export * from './components/Link'; +export * from './components/List'; export * from './components/Select'; export * from './components/Skeleton'; export * from './components/Switch'; @@ -67,3 +69,15 @@ export * from './types'; export { useBreakpoint } from './hooks/useBreakpoint'; export { useBgProvider, useBgConsumer, BgProvider } from './hooks/useBg'; export type { BgContextValue, BgProviderProps } from './hooks/useBg'; + +// Provider +export { BUIProvider } from './provider'; +export type { BUIProviderProps } from './provider'; + +// Analytics +export { useAnalytics, getNodeText } from './analytics'; +export type { + AnalyticsTracker, + AnalyticsEventAttributes, + UseAnalyticsFn, +} from './analytics'; diff --git a/packages/ui/src/provider/BUIProvider.tsx b/packages/ui/src/provider/BUIProvider.tsx new file mode 100644 index 0000000000..6576a9eabb --- /dev/null +++ b/packages/ui/src/provider/BUIProvider.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2026 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 { useMemo, type ReactNode } from 'react'; +import { RouterProvider } from 'react-aria-components'; +import { useInRouterContext, useNavigate, useHref } from 'react-router-dom'; +import { createVersionedValueMap } from '@backstage/version-bridge'; +import { BUIContext } from '../analytics/useAnalytics'; +import type { UseAnalyticsFn } from '../analytics/types'; + +/** @public */ +export type BUIProviderProps = { + useAnalytics?: UseAnalyticsFn; + children: ReactNode; +}; + +/** + * Provides integration capabilities to all descendant BUI components. + * + * @example + * ```tsx + * import { BUIProvider } from '@backstage/ui'; + * import { useAnalytics as useBackstageAnalytics } from '@backstage/core-plugin-api'; + * + * function App() { + * return ( + * + * + * + * ); + * } + * ``` + * + * @public + */ +export function BUIProvider(props: BUIProviderProps) { + const { useAnalytics, children } = props; + const value = useMemo( + () => + createVersionedValueMap({ + 1: { useAnalytics }, + }), + [useAnalytics], + ); + + const content = ( + {children} + ); + + if (useInRouterContext()) { + return {content}; + } + + return content; +} + +function RoutedContent({ children }: { children: ReactNode }) { + const navigate = useNavigate(); + return ( + + {children} + + ); +} diff --git a/packages/ui/src/provider/index.ts b/packages/ui/src/provider/index.ts new file mode 100644 index 0000000000..7a99794d47 --- /dev/null +++ b/packages/ui/src/provider/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2026 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 { BUIProvider } from './BUIProvider'; +export type { BUIProviderProps } from './BUIProvider'; diff --git a/packages/ui/src/recipes/CardsWithList.stories.tsx b/packages/ui/src/recipes/CardsWithList.stories.tsx new file mode 100644 index 0000000000..21113d99a4 --- /dev/null +++ b/packages/ui/src/recipes/CardsWithList.stories.tsx @@ -0,0 +1,268 @@ +/* + * 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 preview from '../../../../.storybook/preview'; +import type { StoryFn } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import { + Card, + CardHeader, + CardBody, + Container, + Grid, + Flex, + Text, + MenuItem, + TagGroup, + Tag, + List, + ListRow, +} from '..'; +import { + RiAccountCircleLine, + RiCloudLine, + RiCodeLine, + RiDeleteBinLine, + RiEdit2Line, + RiGitBranchLine, + RiJavascriptLine, + RiReactjsLine, + RiServerLine, + RiShareBoxLine, + RiShieldLine, + RiTerminalLine, +} from '@remixicon/react'; + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- + +interface ServiceItem { + id: string; + label: string; + description: string; + icon: React.ReactElement; + tags: string[]; +} + +const frontendServices: ServiceItem[] = [ + { + id: 'portal', + label: 'developer-portal', + description: 'Internal developer portal built on Backstage', + icon: , + tags: ['website', 'production'], + }, + { + id: 'design-system', + label: 'design-system', + description: 'Shared UI components and design tokens', + icon: , + tags: ['library', 'production'], + }, + { + id: 'docs-site', + label: 'docs-site', + description: 'Engineering documentation and runbooks', + icon: , + tags: ['website', 'production'], + }, + { + id: 'admin-ui', + label: 'admin-ui', + description: 'Internal tooling for platform administrators', + icon: , + tags: ['website', 'experimental'], + }, + { + id: 'onboarding-flow', + label: 'onboarding-flow', + description: 'New hire onboarding wizard and checklist', + icon: , + tags: ['website', 'experimental'], + }, +]; + +const backendServices: ServiceItem[] = [ + { + id: 'auth', + label: 'authentication-service', + description: 'Handles user authentication, sessions and token refresh', + icon: , + tags: ['service', 'production'], + }, + { + id: 'api-gateway', + label: 'api-gateway', + description: 'Routes and validates all inbound API requests', + icon: , + tags: ['service', 'production'], + }, + { + id: 'search', + label: 'search-indexer', + description: 'Indexes catalog entities for full-text search', + icon: , + tags: ['service', 'experimental'], + }, + { + id: 'ci-runner', + label: 'ci-runner', + description: 'Orchestrates and executes CI pipeline jobs', + icon: , + tags: ['service', 'production'], + }, + { + id: 'infra-provisioner', + label: 'infra-provisioner', + description: 'Terraform-based cloud resource provisioner', + icon: , + tags: ['service', 'experimental'], + }, +]; + +// --------------------------------------------------------------------------- +// Service list card +// --------------------------------------------------------------------------- + +interface ServiceListCardProps { + title: string; + items: ServiceItem[]; + description?: boolean; + icons?: boolean; +} + +const ServiceListCard = ({ + title, + items, + description = false, + icons = true, +}: ServiceListCardProps) => ( + + + + + + {title} + + + + + + + {items.map(item => ( + + }>Edit + }>Share + } color="danger"> + Delete + + + } + customActions={ + + {item.tags.map(tag => ( + {tag} + ))} + + } + > + {item.label} + + ))} + + + +); + +const withRouter = (Story: StoryFn) => ( + + + +); + +const meta = preview.meta({ + title: 'Recipes/Cards with List', + parameters: { + layout: 'fullscreen', + }, +}); + +export const Default = meta.story({ + decorators: [withRouter], + render: () => ( + + + + + + + ), +}); + +export const WithNoIcons = meta.story({ + decorators: [withRouter], + args: { + icons: false, + description: true, + }, + render: args => ( + + + + + + + ), +}); + +export const WithDescription = meta.story({ + args: { + description: true, + }, + render: args => ( + + + + + + + ), +}); diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index d3dfd996aa..7a302e1694 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -154,34 +154,7 @@ export interface UtilityProps extends SpaceProps { } /** - * Base type for the component styles structure - * @public - */ -export type ClassNamesMap = Record; - -/** - * Base type for the component styles structure - * @public - */ -export type DataAttributeValues = readonly (string | number | boolean)[]; - -/** - * Base type for the component styles structure - * @public - */ -export type DataAttributesMap = Record; - -/** - * Base type for the component styles structure - * @public - */ -export interface ComponentDefinition { - classNames: ClassNamesMap; - dataAttributes?: DataAttributesMap; - utilityProps?: string[]; -} - -/** + * Resolved background level stored in context and applied as `data-bg` on DOM elements. * Background type for the neutral bg system. * * Supports neutral levels ('neutral-1' through 'neutral-3') and @@ -190,6 +163,9 @@ export interface ComponentDefinition { * The 'neutral-4' level is not exposed as a prop value -- it is reserved * for leaf component CSS (e.g. Button on a 'neutral-3' surface). * + * This is the resolved/internal representation used by the bg context system. + * For the prop type accepted by container components, use `ProviderBg` instead. + * * @public */ export type ContainerBg = @@ -201,11 +177,13 @@ export type ContainerBg = | 'success'; /** - * Background values accepted by provider components. + * Background values accepted by provider components (Box, Flex, Grid, Card, etc.). * - * Includes all `ContainerBg` values plus `'neutral-auto'` which - * automatically increments the neutral level from the parent context. + * - `'neutral'` — automatically increments the neutral level from the parent context, + * capping at the maximum level. This is always incremental; explicit levels cannot + * be set directly. + * - `'danger'` | `'warning'` | `'success'` — intent backgrounds used as-is. * * @public */ -export type ProviderBg = ContainerBg | 'neutral-auto'; +export type ProviderBg = 'neutral' | 'danger' | 'warning' | 'success'; diff --git a/packages/ui/src/utils/isExternalLink.ts b/packages/ui/src/utils/linkUtils.ts similarity index 85% rename from packages/ui/src/utils/isExternalLink.ts rename to packages/ui/src/utils/linkUtils.ts index a874579ab5..8b2ded55c4 100644 --- a/packages/ui/src/utils/isExternalLink.ts +++ b/packages/ui/src/utils/linkUtils.ts @@ -40,3 +40,12 @@ export function isExternalLink(href?: string): boolean { return false; } + +/** + * Checks if an href is an internal link (not external and not empty). + * + * @internal + */ +export function isInternalLink(href: string | undefined): href is string { + return !!href && !isExternalLink(href); +} diff --git a/packages/version-bridge/CHANGELOG.md b/packages/version-bridge/CHANGELOG.md index 30fdb74d2b..7ca00c7591 100644 --- a/packages/version-bridge/CHANGELOG.md +++ b/packages/version-bridge/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/version-bridge +## 1.0.12 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + ## 1.0.12-next.0 ### Patch Changes diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index e21484500d..1587705367 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/version-bridge", - "version": "1.0.12-next.0", + "version": "1.0.12", "description": "Utilities used by @backstage packages to support multiple concurrent versions", "backstage": { "role": "web-library" diff --git a/packages/yarn-plugin/CHANGELOG.md b/packages/yarn-plugin/CHANGELOG.md index ff9dfc9fb1..e5ee13b8f0 100644 --- a/packages/yarn-plugin/CHANGELOG.md +++ b/packages/yarn-plugin/CHANGELOG.md @@ -1,5 +1,21 @@ # yarn-plugin-backstage +## 0.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/release-manifests@0.0.13 + +## 0.0.9 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.18 + ## 0.0.9-next.0 ### Patch Changes diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index eaa6c06894..b54b422312 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -1,6 +1,6 @@ { "name": "yarn-plugin-backstage", - "version": "0.0.9-next.0", + "version": "0.0.10-next.0", "description": "Yarn plugin for working with Backstage monorepos", "backstage": { "role": "node-library" @@ -46,7 +46,7 @@ "@yarnpkg/builder": "^4.2.1", "fs-extra": "^11.2.0", "nodemon": "^3.0.1", - "snyk-nodejs-lockfile-parser": "^1.58.14", + "snyk-nodejs-lockfile-parser": "^2.0.0", "yaml": "^2.0.0" } } diff --git a/packages/yarn-plugin/src/index.test.ts b/packages/yarn-plugin/src/index.test.ts index 688098a626..4acd6a446f 100644 --- a/packages/yarn-plugin/src/index.test.ts +++ b/packages/yarn-plugin/src/index.test.ts @@ -19,7 +19,7 @@ import { spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; import fs from 'fs-extra'; import yaml from 'yaml'; import { buildDepTreeFromFiles } from 'snyk-nodejs-lockfile-parser'; -import { findPaths } from '@backstage/cli-common'; +import { targetPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; jest.setTimeout(30_000); @@ -86,7 +86,7 @@ describe('Backstage yarn plugin', () => { let initialLockFileContent: string | undefined; beforeAll(async () => { - const { targetRoot } = findPaths(process.cwd()); + const targetRoot = targetPaths.rootDir; await executeCommand('yarn', ['build'], { cwd: joinPath(targetRoot, 'packages/yarn-plugin'), }); diff --git a/packages/yarn-plugin/src/util/getWorkspaceRoot.test.ts b/packages/yarn-plugin/src/util/getWorkspaceRoot.test.ts index 11538b3d50..083baae907 100644 --- a/packages/yarn-plugin/src/util/getWorkspaceRoot.test.ts +++ b/packages/yarn-plugin/src/util/getWorkspaceRoot.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { findPaths, Paths } from '@backstage/cli-common'; - const setPlatform = (platform: string) => { Object.defineProperty(process, `platform`, { configurable: true, @@ -37,7 +35,6 @@ describe('getWorkspaceRoot', () => { `('platform: $platform', ({ platform, native, portable }) => { let realPlatform: string; let getWorkspaceRoot: () => string; - let mockFindPaths: jest.MockedFunction; beforeEach(() => { realPlatform = process.platform; @@ -45,13 +42,10 @@ describe('getWorkspaceRoot', () => { jest.resetModules(); - mockFindPaths = jest.fn(); - - jest.doMock('@backstage/cli-common', () => ({ - ...jest.requireActual('@backstage/cli-common'), - findPaths: mockFindPaths, - })); - + const { + overrideTargetPaths, + } = require('@backstage/cli-common/testUtils'); + overrideTargetPaths(native); getWorkspaceRoot = require('./getWorkspaceRoot').getWorkspaceRoot; }); @@ -60,10 +54,6 @@ describe('getWorkspaceRoot', () => { }); it('returns an appropriately-formatted workspace root path', () => { - mockFindPaths.mockReturnValue({ - targetRoot: native, - } as Paths); - expect(getWorkspaceRoot()).toEqual(portable); }); }); diff --git a/packages/yarn-plugin/src/util/getWorkspaceRoot.ts b/packages/yarn-plugin/src/util/getWorkspaceRoot.ts index d6f3e98e10..57e03daabe 100644 --- a/packages/yarn-plugin/src/util/getWorkspaceRoot.ts +++ b/packages/yarn-plugin/src/util/getWorkspaceRoot.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import { npath, ppath } from '@yarnpkg/fslib'; -import { findPaths } from '@backstage/cli-common'; +import { npath } from '@yarnpkg/fslib'; +import { targetPaths } from '@backstage/cli-common'; export const getWorkspaceRoot = () => { - return npath.toPortablePath( - findPaths(npath.fromPortablePath(ppath.cwd())).targetRoot, - ); + return npath.toPortablePath(targetPaths.rootDir); }; diff --git a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md index a387c4a5a6..cc8c9cde71 100644 --- a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md +++ b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-api-docs-module-protoc-gen-doc +## 0.1.11 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. + ## 0.1.11-next.0 ### Patch Changes diff --git a/plugins/api-docs-module-protoc-gen-doc/package.json b/plugins/api-docs-module-protoc-gen-doc/package.json index ac655b019c..b11973a67f 100644 --- a/plugins/api-docs-module-protoc-gen-doc/package.json +++ b/plugins/api-docs-module-protoc-gen-doc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs-module-protoc-gen-doc", - "version": "0.1.11-next.0", + "version": "0.1.11", "description": "Additional functionalities for the api-docs plugin that renders the output of the protoc-gen-doc", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 091d347c1b..6ab1ee59bb 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-api-docs +## 0.13.5-next.2 + +### Patch Changes + +- ca277ef: Updated dependency `graphiql` to `3.9.0` to address security vulnerability in `markdown-it` package. + Updated dependency `@graphiql/react` to `0.29.0` to match the version used by `graphiql`. + Moved dependency `graphql-config` to `devDependencies` as it is needed only for types. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-catalog@2.0.0-next.2 + +## 0.13.5-next.1 + +### Patch Changes + +- 30e08df: Added default entity content groups for the API docs entity content tabs. The API definition tab defaults to the `documentation` group and the APIs tab defaults to the `development` group. +- Updated dependencies + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.13.5-next.0 + +### Patch Changes + +- 9c9d425: Fixed invisible text in parameter input fields when using dark mode in OpenAPI definition pages +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.13.4 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 7455dae: Use node prefix on native imports +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 629c3ec: Add `tableOptions` and `title` to Components cards of APIs +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-catalog@1.33.0 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.13.4-next.2 ### Patch Changes diff --git a/plugins/api-docs/README-alpha.md b/plugins/api-docs/README-alpha.md index 0f19f51b23..a4c3979787 100644 --- a/plugins/api-docs/README-alpha.md +++ b/plugins/api-docs/README-alpha.md @@ -1,8 +1,6 @@ -# Api Docs +# Api Docs - Extension Reference -> [!WARNING] -> This documentation is made for those using the experimental new Frontend system. -> If you are not using the new frontend system, please go [here](./README.md). +This page contains detailed documentation for all extensions provided by the `@backstage/plugin-api-docs` plugin. For general information about the plugin, see the [README](./README.md). This is an extension for the catalog plugin that provides components to discover and display API entities. APIs define the interface between components, see the [system model](https://backstage.io/docs/features/software-catalog/system-model) for details. diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index 139a97d4c8..f71b796ec4 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -1,8 +1,5 @@ # API Documentation -> Disclaimer: -> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). - This is an extension for the catalog plugin that provides components to discover and display API entities. APIs define the interface between components, see the [system model](https://backstage.io/docs/features/software-catalog/system-model) for details. They are defined in machine readable formats and provide a human readable documentation. @@ -28,77 +25,32 @@ To link that a component provides or consumes an API, see the [`providesApis`](h > The plugin is already added when using `npx @backstage/create-app` so you can skip these steps. -1. Install the API docs plugin - ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-api-docs ``` -2. Add the `ApiExplorerPage` extension to the app: +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). -```tsx -// In packages/app/src/App.tsx +You can enable entity cards and tabs on the catalog entity page through configuration: -import { ApiExplorerPage } from '@backstage/plugin-api-docs'; - -} />; +```yaml +# app-config.yaml +app: + extensions: + - entity-card:api-docs/providing-components: + config: + filter: + kind: api + - entity-card:api-docs/consuming-components: + config: + filter: + kind: api + - entity-content:api-docs/definition + - entity-content:api-docs/apis ``` -3. Add one of the provided widgets to the EntityPage: - -```tsx -// packages/app/src/components/catalog/EntityPage.tsx - -import { - EntityAboutCard, - EntityApiDefinitionCard, - EntityConsumingComponentsCard, - EntityProvidingComponentsCard, -} from '@backstage/plugin-api-docs'; - -const apiPage = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -// ... - -export const entityPage = ( - - // ... - - // ... - -); -``` - -There are other components to discover in [`./src/components`](./src/components) that are also added by the default app. +For the full list of available extensions and their configuration options, see the [README-alpha.md](./README-alpha.md). ## Customizations @@ -388,6 +340,75 @@ import { ApiExplorerPage } from '@backstage/plugin-api-docs'; />; ``` +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + +1. Add the `ApiExplorerPage` extension to the app: + +```tsx +// In packages/app/src/App.tsx + +import { ApiExplorerPage } from '@backstage/plugin-api-docs'; + +} />; +``` + +2. Add one of the provided widgets to the EntityPage: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { + EntityAboutCard, + EntityApiDefinitionCard, + EntityConsumingComponentsCard, + EntityProvidingComponentsCard, +} from '@backstage/plugin-api-docs'; + +const apiPage = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +// ... + +export const entityPage = ( + + // ... + + // ... + +); +``` + +There are other components to discover in [`./src/components`](./src/components) that are also added by the default app. + ## Links - [The Backstage homepage](https://backstage.io) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 7098faae2d..a0d50e78ab 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.13.4-next.2", + "version": "0.13.5-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", @@ -62,13 +62,12 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@graphiql/react": "^0.23.0", + "@graphiql/react": "0.29.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "graphiql": "3.1.1", + "graphiql": "^3.9.0", "graphql": "^16.0.0", - "graphql-config": "^5.0.2", "graphql-ws": "^5.4.1", "swagger-ui-react": "^5.27.1" }, @@ -85,6 +84,7 @@ "@types/highlightjs": "^10.1.0", "@types/react": "^18.0.0", "@types/swagger-ui-react": "^5.0.0", + "graphql-config": "^5.1.6", "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.30.2" diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 3865a53d16..0ee7ac5460 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -6,14 +6,17 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { defaultEntityContentGroups } from '@backstage/plugin-catalog-react/alpha'; import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSXElementConstructor } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -348,7 +351,6 @@ const _default: OverridableFrontendPlugin< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -356,6 +358,7 @@ const _default: OverridableFrontendPlugin< optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -418,7 +421,6 @@ const _default: OverridableFrontendPlugin< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -426,6 +428,7 @@ const _default: OverridableFrontendPlugin< optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -494,29 +497,78 @@ const _default: OverridableFrontendPlugin< config: { initiallySelectedFilter: 'all' | 'owned' | 'starred' | undefined; path: string | undefined; + title: string | undefined; }; configInput: { initiallySelectedFilter?: 'all' | 'owned' | 'starred' | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 44ded088f1..2724619a68 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -43,7 +43,7 @@ const apiDocsNavItem = NavItemBlueprint.make({ params: { title: 'APIs', routeRef: rootRoute, - icon: () => , + icon: () => , }, }); @@ -174,6 +174,7 @@ const apiDocsDefinitionEntityContent = EntityContentBlueprint.make({ params: { path: '/definition', title: 'Definition', + group: 'documentation', filter: { kind: 'api' }, loader: async () => import('./components/ApiDefinitionCard').then(m => ( @@ -191,6 +192,7 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({ params: { path: '/apis', title: 'APIs', + group: 'development', filter: { kind: 'component' }, loader: async () => import('./components/ApisCards').then(m => ( @@ -208,6 +210,8 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({ export default createFrontendPlugin({ pluginId: 'api-docs', + title: 'APIs', + icon: , info: { packageJson: () => import('../package.json') }, routes: { root: rootRoute, diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx index 7279575842..ac7f31ee81 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -173,6 +173,16 @@ const useStyles = makeStyles(theme => ({ { color: theme.palette.warning.dark, }, + [`& input[type=text], + & input[type=password], + & input[type=email], + & input[type=number], + & textarea, + & select`]: { + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + borderColor: theme.palette.divider, + }, }, }, })); diff --git a/plugins/api-docs/src/setupTests.ts b/plugins/api-docs/src/setupTests.ts index 03227e1415..19b6d29795 100644 --- a/plugins/api-docs/src/setupTests.ts +++ b/plugins/api-docs/src/setupTests.ts @@ -23,3 +23,6 @@ Object.defineProperty(global, 'TextEncoder', { Object.defineProperty(global, 'TextDecoder', { value: require('node:util').TextDecoder, }); + +// Use a 15s timeout to accommodate the slowest API docs rendering tests under concurrency. +jest.setTimeout(15_000); diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 02d685d51e..5326ba1886 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-app-backend +## 0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-app-node@0.1.43-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.5.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-app-node@0.1.42 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 4c2fce630e..b1d7e15709 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.5.11-next.0", + "version": "0.5.12-next.1", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index cf75314323..e5a54571e6 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-app-node +## 0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + +## 0.1.42 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + ## 0.1.42-next.0 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 46bae5fa1b..f9f0461ce6 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.42-next.0", + "version": "0.1.43-next.1", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-react/CHANGELOG.md b/plugins/app-react/CHANGELOG.md index 215c353167..b42e72e640 100644 --- a/plugins/app-react/CHANGELOG.md +++ b/plugins/app-react/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-app-react +## 0.2.1-next.1 + +### Patch Changes + +- 2c383b5: Added `AnalyticsImplementationBlueprint` and `AnalyticsImplementationFactory`, migrated from `@backstage/frontend-plugin-api`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.2.0 + +### Minor Changes + +- a2133be: Added new `NavContentNavItem`, `NavContentNavItems`, and `navItems` prop to `NavContentComponentProps` for auto-discovering navigation items from page extensions. The new `navItems` collection supports `take(id)` and `rest()` methods for placing specific items in custom sidebar positions, as well as `withComponent(Component)` which returns a `NavContentNavItemsWithComponent` for rendering items directly as elements. The existing `items` prop is now deprecated in favor of `navItems`. + +### Patch Changes + +- ef6916e: Added `IconElement` type as a replacement for the deprecated `IconComponent`. The `IconsApi` now has a new `icon()` method that returns `IconElement`, while the existing `getIcon()` method is deprecated. The `IconBundleBlueprint` now accepts both `IconComponent` and `IconElement` values. +- 409af72: Internal refactor to move implementation of blueprints from `@backstage/frontend-plugin-api` to this package. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.1-next.0 ### Patch Changes diff --git a/plugins/app-react/package.json b/plugins/app-react/package.json index 5661a196d1..f36f500a75 100644 --- a/plugins/app-react/package.json +++ b/plugins/app-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-react", - "version": "0.1.1-next.0", + "version": "0.2.1-next.1", "description": "Web library for the app plugin", "backstage": { "role": "web-library", diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md index 78f4bd3bfb..a4e63e254b 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnalyticsImplementation } from '@backstage/frontend-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; import { AppTheme } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; @@ -10,12 +12,47 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { IdentityApi } from '@backstage/frontend-plugin-api'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SwappableComponentRef } from '@backstage/frontend-plugin-api'; import { TranslationMessages } from '@backstage/frontend-plugin-api'; import { TranslationResource } from '@backstage/frontend-plugin-api'; +import { TypesToApiRefs } from '@backstage/frontend-plugin-api'; + +// @public +export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{ + kind: 'analytics'; + params: ( + params: AnalyticsImplementationFactory, + ) => ExtensionBlueprintParams>; + output: ExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + factory: ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + }; +}>; + +// @public (undocumented) +export type AnalyticsImplementationFactory< + Deps extends { + [name in string]: unknown; + } = {}, +> = { + deps: TypesToApiRefs; + factory(deps: Deps): AnalyticsImplementation; +}; // @public export const AppRootWrapperBlueprint: ExtensionBlueprint<{ @@ -45,11 +82,11 @@ export const AppRootWrapperBlueprint: ExtensionBlueprint<{ export const IconBundleBlueprint: ExtensionBlueprint<{ kind: 'icon-bundle'; params: { - icons: { [key in string]: IconComponent }; + icons: { [key in string]: IconComponent | IconElement }; }; output: ExtensionDataRef< { - [x: string]: IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {} @@ -60,7 +97,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ dataRefs: { icons: ConfigurableExtensionDataRef< { - [x: string]: IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {} @@ -98,6 +135,7 @@ export type NavContentComponent = ( // @public export interface NavContentComponentProps { + // @deprecated items: Array<{ icon: IconComponent; title: string; @@ -105,6 +143,32 @@ export interface NavContentComponentProps { to: string; text: string; }>; + navItems: NavContentNavItems; +} + +// @public +export interface NavContentNavItem { + href: string; + icon: IconElement; + node: AppNode; + routeRef: RouteRef; + title: string; +} + +// @public +export interface NavContentNavItems { + clone(): NavContentNavItems; + rest(): NavContentNavItem[]; + take(id: string): NavContentNavItem | undefined; + withComponent( + Component: ComponentType, + ): NavContentNavItemsWithComponent; +} + +// @public +export interface NavContentNavItemsWithComponent { + rest(options?: { sortBy?: 'title' }): JSX.Element[]; + take(id: string): JSX.Element | null; } // @public diff --git a/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.test.ts b/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.test.ts new file mode 100644 index 0000000000..06ad9b877e --- /dev/null +++ b/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { AnalyticsImplementationBlueprint } from './AnalyticsImplementationBlueprint'; + +describe('AnalyticsBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const factory = { + deps: {}, + factory: () => ({ captureEvent: () => {} }), + }; + + const extension = AnalyticsImplementationBlueprint.make({ + params: define => define(factory), + name: 'test', + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "T": undefined, + "attachTo": { + "id": "api:app/analytics", + "input": "implementations", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "analytics", + "name": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.ts b/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.ts new file mode 100644 index 0000000000..d85c926ee0 --- /dev/null +++ b/plugins/app-react/src/blueprints/AnalyticsImplementationBlueprint.ts @@ -0,0 +1,56 @@ +/* + * 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 { + AnalyticsImplementation, + TypesToApiRefs, + createExtensionBlueprint, + createExtensionBlueprintParams, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; + +/** @public */ +export type AnalyticsImplementationFactory< + Deps extends { [name in string]: unknown } = {}, +> = { + deps: TypesToApiRefs; + factory(deps: Deps): AnalyticsImplementation; +}; + +const factoryDataRef = + createExtensionDataRef().with({ + id: 'core.analytics.factory', + }); + +/** + * Creates analytics implementations. + * + * @public + */ +export const AnalyticsImplementationBlueprint = createExtensionBlueprint({ + kind: 'analytics', + attachTo: { id: 'api:app/analytics', input: 'implementations' }, + output: [factoryDataRef], + dataRefs: { + factory: factoryDataRef, + }, + defineParams: ( + params: AnalyticsImplementationFactory, + ) => createExtensionBlueprintParams(params as AnalyticsImplementationFactory), + *factory(params) { + yield factoryDataRef(params); + }, +}); diff --git a/plugins/app-react/src/blueprints/IconBundleBlueprint.ts b/plugins/app-react/src/blueprints/IconBundleBlueprint.ts index ce340c2aeb..99c6abcd72 100644 --- a/plugins/app-react/src/blueprints/IconBundleBlueprint.ts +++ b/plugins/app-react/src/blueprints/IconBundleBlueprint.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconComponent, IconElement } from '@backstage/frontend-plugin-api'; import { createExtensionBlueprint, createExtensionDataRef, } from '@backstage/frontend-plugin-api'; const iconsDataRef = createExtensionDataRef<{ - [key in string]: IconComponent; + [key in string]: IconComponent | IconElement; }>().with({ id: 'core.icons' }); /** @@ -33,9 +33,9 @@ export const IconBundleBlueprint = createExtensionBlueprint({ kind: 'icon-bundle', attachTo: { id: 'api:app/icons', input: 'icons' }, output: [iconsDataRef], - factory: (params: { icons: { [key in string]: IconComponent } }) => [ - iconsDataRef(params.icons), - ], + factory: (params: { + icons: { [key in string]: IconComponent | IconElement }; + }) => [iconsDataRef(params.icons)], dataRefs: { icons: iconsDataRef, }, diff --git a/plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx b/plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx index a113a2f21d..cb782409b1 100644 --- a/plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx +++ b/plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx @@ -14,12 +14,59 @@ * limitations under the License. */ -import { createRouteRef } from '@backstage/frontend-plugin-api'; -import { NavContentBlueprint } from './NavContentBlueprint'; +import { AppNode, createRouteRef } from '@backstage/frontend-plugin-api'; +import { + NavContentBlueprint, + NavContentNavItem, + NavContentNavItems, +} from './NavContentBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { render, screen } from '@testing-library/react'; const routeRef = createRouteRef(); +function mockNode(id: string): AppNode { + return { spec: { id } } as AppNode; +} + +function mockNavItems(items: NavContentNavItem[]): NavContentNavItems { + const taken = new Set(); + return { + take(id: string) { + const item = items.find(i => i.node.spec.id === id); + if (item) { + taken.add(id); + } + return item; + }, + rest: () => items.filter(i => !taken.has(i.node.spec.id)), + clone() { + return mockNavItems(items); + }, + withComponent(Component: (props: NavContentNavItem) => JSX.Element) { + return { + take: (id: string) => { + const item = items.find(i => i.node.spec.id === id); + if (item) { + taken.add(id); + return ; + } + return null; + }, + rest: (options?: { sortBy?: 'title' }) => { + const remaining = items.filter(i => !taken.has(i.node.spec.id)); + if (options?.sortBy === 'title') { + remaining.sort((a, b) => a.title.localeCompare(b.title)); + } + return remaining.map(item => ( + + )); + }, + }; + }, + }; +} + describe('NavContentBlueprint', () => { it('should create an extension with sensible defaults', () => { const extension = NavContentBlueprint.make({ @@ -52,22 +99,7 @@ describe('NavContentBlueprint', () => { `); }); - it('should return a valid component', () => { - const extension = NavContentBlueprint.make({ - name: 'test', - params: { - component: () =>

    Nav content
    , - }, - }); - - const tester = createExtensionTester(extension); - - expect( - tester.get(NavContentBlueprint.dataRefs.component)({ items: [] }), - ).toEqual(
    Nav content
    ); - }); - - it('should return a valid component with items', () => { + it('should return a valid component with legacy items', () => { const extension = NavContentBlueprint.make({ name: 'test', params: { @@ -88,6 +120,7 @@ describe('NavContentBlueprint', () => { expect( tester.get(NavContentBlueprint.dataRefs.component)({ + navItems: mockNavItems([]), items: [ { to: '/', @@ -109,4 +142,128 @@ describe('NavContentBlueprint', () => { , ); }); + + it('should return a valid component with navItems', () => { + const items: NavContentNavItem[] = [ + { + node: mockNode('page:home'), + href: '/', + title: 'Home', + icon: home, + routeRef, + }, + { + node: mockNode('page:catalog'), + href: '/catalog', + title: 'Catalog', + icon: catalog, + routeRef, + }, + { + node: mockNode('page:docs'), + href: '/docs', + title: 'Docs', + icon: docs, + routeRef, + }, + ]; + + const extension = NavContentBlueprint.make({ + name: 'test', + params: { + component: ({ navItems }) => ( +
    + {navItems.rest().map(item => ( + + {item.title} + + ))} +
    + ), + }, + }); + + const tester = createExtensionTester(extension); + + expect( + tester.get(NavContentBlueprint.dataRefs.component)({ + navItems: mockNavItems(items), + items: [], + }), + ).toEqual( +
    + {[ + + Home + , + + Catalog + , + + Docs + , + ]} +
    , + ); + }); + + it('should support withComponent for take and rest', () => { + const items: NavContentNavItem[] = [ + { + node: mockNode('page:home'), + href: '/', + title: 'Home', + icon: home, + routeRef, + }, + { + node: mockNode('page:catalog'), + href: '/catalog', + title: 'Catalog', + icon: catalog, + routeRef, + }, + { + node: mockNode('page:docs'), + href: '/docs', + title: 'Docs', + icon: docs, + routeRef, + }, + ]; + + const extension = NavContentBlueprint.make({ + name: 'test', + params: { + component: ({ navItems }) => { + const nav = navItems.withComponent(item => ( + {item.title} + )); + return ( +
    +
    {nav.take('page:home')}
    + +
    + ); + }, + }, + }); + + const tester = createExtensionTester(extension); + const Component = tester.get(NavContentBlueprint.dataRefs.component); + + render(); + + const homeLink = screen.getByText('Home'); + expect(homeLink).toBeInTheDocument(); + expect(homeLink.closest('header')).toBeTruthy(); + + const catalogLink = screen.getByText('Catalog'); + expect(catalogLink).toBeInTheDocument(); + expect(catalogLink.closest('nav')).toBeTruthy(); + + const docsLink = screen.getByText('Docs'); + expect(docsLink).toBeInTheDocument(); + expect(docsLink.closest('nav')).toBeTruthy(); + }); }); diff --git a/plugins/app-react/src/blueprints/NavContentBlueprint.ts b/plugins/app-react/src/blueprints/NavContentBlueprint.ts index 0f21ed832a..f88ba48a38 100644 --- a/plugins/app-react/src/blueprints/NavContentBlueprint.ts +++ b/plugins/app-react/src/blueprints/NavContentBlueprint.ts @@ -14,12 +14,68 @@ * limitations under the License. */ -import { IconComponent, RouteRef } from '@backstage/frontend-plugin-api'; +import { ComponentType } from 'react'; +import { + AppNode, + IconComponent, + IconElement, + RouteRef, +} from '@backstage/frontend-plugin-api'; import { createExtensionBlueprint, createExtensionDataRef, } from '@backstage/frontend-plugin-api'; +/** + * A navigation item auto-discovered from a page extension in the app. + * + * @public + */ +export interface NavContentNavItem { + /** The app node of the page extension that this nav item points to */ + node: AppNode; + /** The resolved route path */ + href: string; + /** The display title */ + title: string; + /** The display icon */ + icon: IconElement; + /** The route ref of the source page */ + routeRef: RouteRef; +} + +/** + * A pre-bound renderer that wraps {@link NavContentNavItems} with a component, + * so that `take` and `rest` return rendered elements directly. + * + * @public + */ +export interface NavContentNavItemsWithComponent { + /** Render and take a specific item by extension ID. Returns null if not found. */ + take(id: string): JSX.Element | null; + /** Render all remaining items not yet taken, optionally sorted. */ + rest(options?: { sortBy?: 'title' }): JSX.Element[]; +} + +/** + * A collection of nav items that supports picking specific items by ID + * and retrieving whatever remains. Created fresh for each render. + * + * @public + */ +export interface NavContentNavItems { + /** Take an item by extension ID, removing it from the collection. */ + take(id: string): NavContentNavItem | undefined; + /** All items not yet taken. */ + rest(): NavContentNavItem[]; + /** Create a copy of the collection preserving the current taken state. */ + clone(): NavContentNavItems; + /** Create a renderer that wraps take/rest to return pre-rendered elements. */ + withComponent( + Component: ComponentType, + ): NavContentNavItemsWithComponent; +} + /** * The props for the {@link NavContentComponent}. * @@ -27,20 +83,21 @@ import { */ export interface NavContentComponentProps { /** - * The nav items available to the component. These are all the items created - * with the {@link @backstage/frontend-plugin-api#NavItemBlueprint} in the app. + * Nav items auto-discovered from page extensions, with take/rest semantics + * for placing specific items in specific positions. + */ + navItems: NavContentNavItems; + + /** + * Flat list of nav items for simple rendering. Use `navItems` for more + * control over item placement. * - * In addition to the original properties from the nav items, these also - * include a resolved route path as `to`, and duplicated `title` as `text` to - * simplify rendering. + * @deprecated Use `navItems` instead. */ items: Array<{ - // Original props from nav items icon: IconComponent; title: string; routeRef: RouteRef; - - // Additional props to simplify item rendering to: string; text: string; }>; diff --git a/plugins/app-react/src/blueprints/index.ts b/plugins/app-react/src/blueprints/index.ts index 15a0fdcd47..ad9226f2a6 100644 --- a/plugins/app-react/src/blueprints/index.ts +++ b/plugins/app-react/src/blueprints/index.ts @@ -14,12 +14,19 @@ * limitations under the License. */ +export { + AnalyticsImplementationBlueprint, + type AnalyticsImplementationFactory, +} from './AnalyticsImplementationBlueprint'; export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; export { IconBundleBlueprint } from './IconBundleBlueprint'; export { NavContentBlueprint } from './NavContentBlueprint'; export type { NavContentComponent, NavContentComponentProps, + NavContentNavItem, + NavContentNavItemsWithComponent, + NavContentNavItems, } from './NavContentBlueprint'; export { RouterBlueprint } from './RouterBlueprint'; export { SignInPageBlueprint } from './SignInPageBlueprint'; diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index 7ab41d9b23..0a8b35a21a 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-app-visualizer +## 0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.2.0 + +### Minor Changes + +- ef6916e: Migrated to use `SubPageBlueprint` for tabbed navigation and added a copy-tree-as-JSON plugin header action using `PluginHeaderActionBlueprint`. The plugin now specifies a `title` and `icon`. + +### Patch Changes + +- cb090b4: Bump react-aria-components to v1.14.0 +- c38b74d: Internal updates for blueprint moves to `@backstage/plugin-app-react`. +- 4137a43: Updated CSS token references to use renamed `--bui-border-2` token. +- 4d50e1f: Improved rendering performance of the details page. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/dev/index.ts b/plugins/app-visualizer/dev/index.ts index fa49e3360b..5a5222ffbf 100644 --- a/plugins/app-visualizer/dev/index.ts +++ b/plugins/app-visualizer/dev/index.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -import ReactDOM from 'react-dom/client'; -import { createApp } from '@backstage/frontend-defaults'; +import { createDevApp } from '@backstage/frontend-dev-utils'; import { default as plugin } from '../src'; -const app = createApp({ - features: [plugin], -}); - -ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot()); +createDevApp({ features: [plugin] }); diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index ca5a7f4e12..bf71701775 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.28-next.1", + "version": "0.2.1-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", @@ -43,7 +43,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/frontend-defaults": "workspace:^", + "@backstage/frontend-dev-utils": "workspace:^", "@types/react": "^18.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index 180507677e..9ee06eccb0 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -4,8 +4,12 @@ ```ts import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -42,24 +46,200 @@ const visualizerPlugin: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef; + noHeader?: boolean; + }; + }>; + 'plugin-header-action:app-visualizer': OverridableExtensionDefinition<{ + kind: 'plugin-header-action'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: (params: { + loader: () => Promise; + }) => ExtensionBlueprintParams<{ + loader: () => Promise; + }>; + }>; + 'sub-page:app-visualizer/details': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'details'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: {}; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + 'sub-page:app-visualizer/text': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'text'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + 'sub-page:app-visualizer/tree': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'tree'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + title?: string | undefined; + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; loader: () => Promise; routeRef?: RouteRef; }; diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index c005ccb18f..7bdd1e8900 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -15,8 +15,6 @@ */ import { Content, Header, HeaderTabs, Page } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; -import { appTreeApiRef } from '@backstage/frontend-plugin-api'; import { Flex } from '@backstage/ui'; import { useCallback, useEffect, useMemo } from 'react'; import { DetailedVisualizer } from './DetailedVisualizer'; @@ -31,31 +29,28 @@ import { } from 'react-router-dom'; export function AppVisualizerPage() { - const appTreeApi = useApi(appTreeApiRef); - const { tree } = appTreeApi.getTree(); - const tabs = useMemo( () => [ { id: 'tree', path: 'tree', label: 'Tree', - element: , + element: , }, { id: 'detailed', path: 'detailed', label: 'Detailed', - element: , + element: , }, { id: 'text', path: 'text', label: 'Text', - element: , + element: , }, ], - [tree], + [], ); const location = useLocation(); diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 90ab44b626..e88f994627 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -16,15 +16,23 @@ import { AppNode, - AppTree, ExtensionDataRef, coreExtensionData, ApiBlueprint, NavItemBlueprint, useApi, routeResolutionApiRef, + appTreeApiRef, } from '@backstage/frontend-plugin-api'; -import { Box, Flex, Link, Text, Tooltip, TooltipTrigger } from '@backstage/ui'; +import { + Box, + Flex, + FullPage, + Link, + Text, + Tooltip, + TooltipTrigger, +} from '@backstage/ui'; import { RiInputField as InputIcon, RiCloseCircleLine as DisabledIcon, @@ -351,24 +359,29 @@ function Legend() { ); } -export function DetailedVisualizer({ tree }: { tree: AppTree }) { - return ( - - - - +export function DetailedVisualizer() { + const appTreeApi = useApi(appTreeApiRef); + const { tree } = appTreeApi.getTree(); - - - - + return ( + + + + + + + + + + + ); } diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx index 99fb20ac28..954c1b7a5c 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; +import { AppNode, useApi, appTreeApiRef } from '@backstage/frontend-plugin-api'; import { Box, Checkbox } from '@backstage/ui'; import { ReactNode, useState } from 'react'; @@ -77,7 +77,9 @@ function nodeToText( ]); } -export function TextVisualizer({ tree }: { tree: AppTree }) { +export function TextVisualizer() { + const appTreeApi = useApi(appTreeApiRef); + const { tree } = appTreeApi.getTree(); const [showOutputs, setShowOutputs] = useState(false); const [showDisabled, setShowDisabled] = useState(false); diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index d3a9145bb3..b851df0b26 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -18,8 +18,13 @@ import { DependencyGraph, DependencyGraphTypes, } from '@backstage/core-components'; -import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; -import { Flex } from '@backstage/ui'; +import { + AppNode, + AppTree, + useApi, + appTreeApiRef, +} from '@backstage/frontend-plugin-api'; +import { Flex, FullPage } from '@backstage/ui'; import { useLayoutEffect, useMemo, useRef, useState } from 'react'; type NodeType = @@ -137,28 +142,25 @@ export function Node(props: { node: NodeType }) { ); } -export function TreeVisualizer({ tree }: { tree: AppTree }) { +export function TreeVisualizer() { + const appTreeApi = useApi(appTreeApiRef); + const { tree } = appTreeApi.getTree(); const graphData = useMemo(() => resolveGraphData(tree), [tree]); return ( - - - + + + + + ); } diff --git a/plugins/app-visualizer/src/components/CopyTreeButton.tsx b/plugins/app-visualizer/src/components/CopyTreeButton.tsx new file mode 100644 index 0000000000..4b430e65dc --- /dev/null +++ b/plugins/app-visualizer/src/components/CopyTreeButton.tsx @@ -0,0 +1,63 @@ +/* + * 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 { useState } from 'react'; +import { + useApi, + appTreeApiRef, + type AppNode, +} from '@backstage/frontend-plugin-api'; +import { Button } from '@backstage/ui'; +import { RiFileCopyLine, RiCheckLine } from '@remixicon/react'; + +function nodeToJson(node: AppNode): object { + const attachments: Record = {}; + for (const [input, children] of node.edges.attachments) { + attachments[input] = children.map(nodeToJson); + } + + return { + id: node.spec.id, + plugin: node.spec.plugin.pluginId, + disabled: node.spec.disabled || undefined, + ...(Object.keys(attachments).length > 0 ? { attachments } : {}), + }; +} + +export function CopyTreeButton() { + const appTreeApi = useApi(appTreeApiRef); + const [copied, setCopied] = useState(false); + + const handlePress = () => { + const { tree } = appTreeApi.getTree(); + const json = JSON.stringify(nodeToJson(tree.root), null, 2); + window.navigator.clipboard.writeText(json).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( + + ); +} diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index c9c7857c77..9fa7b2cd72 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -19,8 +19,10 @@ import { createRouteRef, NavItemBlueprint, PageBlueprint, + PluginHeaderActionBlueprint, + SubPageBlueprint, } from '@backstage/frontend-plugin-api'; -import { RiEyeLine as VisualizerIcon } from '@remixicon/react'; +import { RiEyeLine } from '@remixicon/react'; const rootRouteRef = createRouteRef(); @@ -28,17 +30,63 @@ const appVisualizerPage = PageBlueprint.make({ params: { path: '/visualizer', routeRef: rootRouteRef, + title: 'Visualizer', + }, +}); + +const treeRouteRef = createRouteRef(); +const detailedRouteRef = createRouteRef(); +const textRouteRef = createRouteRef(); + +const appVisualizerTreePage = SubPageBlueprint.make({ + name: 'tree', + params: { + path: 'tree', + routeRef: treeRouteRef, + title: 'Tree', loader: () => - import('./components/AppVisualizerPage').then(m => ( - + import('./components/AppVisualizerPage/TreeVisualizer').then(m => ( + )), }, }); +const appVisualizerDetailedPage = SubPageBlueprint.make({ + name: 'details', + params: { + path: 'details', + routeRef: detailedRouteRef, + title: 'Detailed', + loader: () => + import('./components/AppVisualizerPage/DetailedVisualizer').then(m => ( + + )), + }, +}); +const appVisualizerTextPage = SubPageBlueprint.make({ + name: 'text', + params: { + path: 'text', + routeRef: textRouteRef, + title: 'Text', + loader: () => + import('./components/AppVisualizerPage/TextVisualizer').then(m => ( + + )), + }, +}); + +const copyTreeAsJson = PluginHeaderActionBlueprint.make({ + params: defineParams => + defineParams({ + loader: () => + import('./components/CopyTreeButton').then(m => ), + }), +}); export const appVisualizerNavItem = NavItemBlueprint.make({ params: { title: 'Visualizer', - icon: () => , + icon: () => , routeRef: rootRouteRef, }, }); @@ -46,6 +94,15 @@ export const appVisualizerNavItem = NavItemBlueprint.make({ /** @public */ export const visualizerPlugin = createFrontendPlugin({ pluginId: 'app-visualizer', + title: 'App Visualizer', + icon: , info: { packageJson: () => import('../package.json') }, - extensions: [appVisualizerPage, appVisualizerNavItem], + extensions: [ + appVisualizerPage, + appVisualizerTreePage, + appVisualizerDetailedPage, + appVisualizerTextPage, + appVisualizerNavItem, + copyTreeAsJson, + ], }); diff --git a/plugins/app/CHANGELOG.md b/plugins/app/CHANGELOG.md index 11a0e9228f..6b743447f1 100644 --- a/plugins/app/CHANGELOG.md +++ b/plugins/app/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-app +## 0.4.1-next.2 + +### Patch Changes + +- 12d8afe: Added `BUIProvider` from `@backstage/ui` to the app root, enabling BUI components to fire analytics events through the Backstage analytics system. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-app-react@0.2.1-next.1 + +## 0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.4.1-next.0 + +### Patch Changes + +- 909c742: Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.4.0 + +### Minor Changes + +- ef6916e: Added `SubPageBlueprint` for creating sub-page tabs, `PluginHeaderActionBlueprint` and `PluginHeaderActionsApi` for plugin-scoped header actions, and `PageLayout` as a swappable component. The `PageBlueprint` now supports sub-pages with tabbed navigation, page title, icon, and header actions. Plugins can now specify a `title` and `icon` in `createFrontendPlugin`. +- 7edb810: **BREAKING**: Extensions created with the following blueprints must now be provided via an override or a module for the `app` plugin. Extensions from other plugins will now trigger a warning in the app and be ignored. + + - `IconBundleBlueprint` + - `NavContentBlueprint` + - `RouterBlueprint` + - `SignInPageBlueprint` + - `SwappableComponentBlueprint` + - `ThemeBlueprint` + - `TranslationBlueprint` + +### Patch Changes + +- a2133be: Added new `NavContentNavItem`, `NavContentNavItems`, and `navItems` prop to `NavContentComponentProps` for auto-discovering navigation items from page extensions. The new `navItems` collection supports `take(id)` and `rest()` methods for placing specific items in custom sidebar positions, as well as `withComponent(Component)` which returns a `NavContentNavItemsWithComponent` for rendering items directly as elements. The existing `items` prop is now deprecated in favor of `navItems`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-app-react@0.2.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + ## 0.4.0-next.2 ### Patch Changes diff --git a/plugins/app/package.json b/plugins/app/package.json index 44b3ff62c7..0fe0ebfcee 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app", - "version": "0.4.0-next.2", + "version": "0.4.1-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "app", @@ -59,6 +59,7 @@ "@backstage/plugin-permission-react": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", + "@backstage/ui": "workspace:^", "@backstage/version-bridge": "workspace:^", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 5b96fa04f7..3f14b6d009 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -14,10 +14,12 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { NavContentComponent } from '@backstage/plugin-app-react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { PluginWrapperDefinition } from '@backstage/frontend-plugin-api'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SignInPageProps } from '@backstage/plugin-app-react'; @@ -476,7 +478,7 @@ const appPlugin: OverridableFrontendPlugin< icons: ExtensionInput< ConfigurableExtensionDataRef< { - [x: string]: IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {} @@ -588,6 +590,30 @@ const appPlugin: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:app/plugin-header-actions': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: { + actions: ExtensionInput< + ConfigurableExtensionDataRef, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'api'; + name: 'plugin-header-actions'; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:app/plugin-wrapper': OverridableExtensionDefinition<{ config: {}; configInput: {}; @@ -595,11 +621,7 @@ const appPlugin: OverridableFrontendPlugin< inputs: { wrappers: ExtensionInput< ConfigurableExtensionDataRef< - () => Promise<{ - component: ComponentType<{ - children: ReactNode; - }>; - }>, + () => Promise, 'core.plugin-wrapper.loader', {} >, @@ -937,6 +959,62 @@ const appPlugin: OverridableFrontendPlugin< : never; }>; }>; + 'component:app/core-page-layout': OverridableExtensionDefinition<{ + kind: 'component'; + name: 'core-page-layout'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + { + ref: SwappableComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.swappableComponent', + {} + >; + inputs: {}; + params: >(params: { + component: Ref extends SwappableComponentRef< + any, + infer IExternalComponentProps + > + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element | null) + : never; + loader: Ref extends SwappableComponentRef< + infer IInnerComponentProps, + any + > + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }) => ExtensionBlueprintParams<{ + component: Ref extends SwappableComponentRef< + any, + infer IExternalComponentProps + > + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element | null) + : never; + loader: Ref extends SwappableComponentRef< + infer IInnerComponentProps, + any + > + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }>; + }>; 'component:app/core-progress': OverridableExtensionDefinition<{ kind: 'component'; name: 'core-progress'; diff --git a/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.test.tsx b/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.test.tsx new file mode 100644 index 0000000000..9023c86ce8 --- /dev/null +++ b/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.test.tsx @@ -0,0 +1,72 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { DefaultPluginHeaderActionsApi } from './DefaultPluginHeaderActionsApi'; + +describe('DefaultPluginHeaderActionsApi', () => { + it('should return actions for a specific plugin', () => { + const api = DefaultPluginHeaderActionsApi.fromActions([ + { + element: , + pluginId: 'plugin-a', + }, + { + element: , + pluginId: 'plugin-b', + }, + ]); + + expect(api.getPluginHeaderActions('plugin-a')).toHaveLength(1); + expect(api.getPluginHeaderActions('plugin-b')).toHaveLength(1); + + render(<>{api.getPluginHeaderActions('plugin-a')}); + expect( + screen.getByRole('button', { name: 'Action A' }), + ).toBeInTheDocument(); + }); + + it('should return an empty array for unknown plugins', () => { + const api = DefaultPluginHeaderActionsApi.fromActions([ + { + element: Action, + pluginId: 'plugin-a', + }, + ]); + + expect(api.getPluginHeaderActions('unknown-plugin')).toEqual([]); + }); + + it('should group multiple actions by plugin', () => { + const api = DefaultPluginHeaderActionsApi.fromActions([ + { + element: , + pluginId: 'plugin-a', + }, + { + element: , + pluginId: 'plugin-a', + }, + ]); + + const actions = api.getPluginHeaderActions('plugin-a'); + expect(actions).toHaveLength(2); + + render(<>{actions}); + expect(screen.getByRole('button', { name: 'First' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Second' })).toBeInTheDocument(); + }); +}); diff --git a/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx b/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx new file mode 100644 index 0000000000..ae114e4e24 --- /dev/null +++ b/plugins/app/src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2026 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 { JSX } from 'react'; +import { type PluginHeaderActionsApi } from '@backstage/frontend-plugin-api'; + +// Stable reference +const EMPTY_ACTIONS = new Array(); + +type ActionInput = { + element: JSX.Element; + pluginId: string; +}; + +/** + * Default implementation of PluginHeaderActionsApi. + * + * @internal + */ +export class DefaultPluginHeaderActionsApi implements PluginHeaderActionsApi { + constructor( + private readonly actionsByPlugin: Map>, + ) {} + + getPluginHeaderActions(pluginId: string): Array { + return this.actionsByPlugin.get(pluginId) ?? EMPTY_ACTIONS; + } + + static fromActions( + actions: Array, + ): DefaultPluginHeaderActionsApi { + const actionsByPlugin = new Map>(); + + for (const action of actions) { + let pluginActions = actionsByPlugin.get(action.pluginId); + if (!pluginActions) { + pluginActions = []; + actionsByPlugin.set(action.pluginId, pluginActions); + } + + pluginActions.push(action.element); + } + + return new DefaultPluginHeaderActionsApi(actionsByPlugin); + } +} diff --git a/packages/ui/src/components/HeaderPage/index.tsx b/plugins/app/src/apis/PluginHeaderActionsApi/index.ts similarity index 78% rename from packages/ui/src/components/HeaderPage/index.tsx rename to plugins/app/src/apis/PluginHeaderActionsApi/index.ts index 7b277149f1..be7906a2a7 100644 --- a/packages/ui/src/components/HeaderPage/index.tsx +++ b/plugins/app/src/apis/PluginHeaderActionsApi/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { HeaderPage } from './HeaderPage'; -export { HeaderPageDefinition } from './definition'; -export type { HeaderPageProps, HeaderPageBreadcrumb } from './types'; +export { DefaultPluginHeaderActionsApi } from './DefaultPluginHeaderActionsApi'; diff --git a/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.test.tsx b/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.test.tsx index c723222f2f..bbf9ce6294 100644 --- a/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.test.tsx +++ b/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.test.tsx @@ -16,6 +16,34 @@ import { render, screen } from '@testing-library/react'; import { DefaultPluginWrapperApi } from './DefaultPluginWrapperApi'; +import { PluginWrapperDefinition } from '@backstage/frontend-plugin-api'; +import { ReactNode, useState } from 'react'; +import userEvent from '@testing-library/user-event'; + +type TestInc = { count: number; increment: () => void }; + +function useTestInc(): TestInc { + const [value, setValue] = useState(0); + return { + count: value, + increment: () => setValue(val => val + 1), + }; +} + +function makeTestIncWrapper( + label: string = '', + renderSpy?: () => void, +): (props: { children: ReactNode; value: TestInc }) => JSX.Element { + return ({ children, value }: { children: ReactNode; value: TestInc }) => { + renderSpy?.(); + return ( +
    + Wrapper{label}#{value.count} {children} + +
    + ); + }; +} describe('DefaultPluginWrapperApi', () => { it('should wrap multiple components with a single wrapper', async () => { @@ -36,8 +64,10 @@ describe('DefaultPluginWrapperApi', () => { expect(Wrapper2).toBeDefined(); expect(Wrapper3).toBeDefined(); + const RootWrapper = api.getRootWrapper(); + render( - <> +
    1
    @@ -47,7 +77,7 @@ describe('DefaultPluginWrapperApi', () => {
    3
    - , +
    , ); await expect(screen.findByText('Wrapper(1)')).resolves.toBeInTheDocument(); @@ -77,15 +107,17 @@ describe('DefaultPluginWrapperApi', () => { expect(Wrapper1).toBeDefined(); expect(Wrapper2).toBeDefined(); + const RootWrapper = api.getRootWrapper(); + render( - <> +
    1
    2
    - , +
    , ); await expect( @@ -95,4 +127,153 @@ describe('DefaultPluginWrapperApi', () => { screen.findByText('WrapperB(WrapperA(2))'), ).resolves.toBeInTheDocument(); }); + + it('should share a single value across multiple wrappers', async () => { + const api = DefaultPluginWrapperApi.fromWrappers([ + { + loader: async (): Promise> => ({ + component: ({ children, value }) => ( + <> + Wrapper({children}:{value}) + + ), + useWrapperValue: () => 'foo', + }), + pluginId: 'plugin-1', + }, + ]); + + const Wrapper1 = api.getPluginWrapper('plugin-1')!; + const Wrapper2 = api.getPluginWrapper('plugin-1')!; + + expect(Wrapper1).toBeDefined(); + expect(Wrapper2).toBeDefined(); + + const RootWrapper = api.getRootWrapper(); + + render( + +
    + 1 +
    +
    + 2 +
    +
    , + ); + + await expect( + screen.findByText('Wrapper(1:foo)'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByText('Wrapper(2:foo)'), + ).resolves.toBeInTheDocument(); + }); + + it('should share a single stateful value across multiple wrappers', async () => { + const api = DefaultPluginWrapperApi.fromWrappers([ + { + loader: async (): Promise> => ({ + component: makeTestIncWrapper(), + useWrapperValue: useTestInc, + }), + pluginId: 'plugin-1', + }, + ]); + + const Wrapper1 = api.getPluginWrapper('plugin-1')!; + const Wrapper2 = api.getPluginWrapper('plugin-1')!; + + expect(Wrapper1).toBeDefined(); + expect(Wrapper2).toBeDefined(); + + const RootWrapper = api.getRootWrapper(); + + render( + + X + Y + , + ); + + await expect(screen.findByText('Wrapper#0 X')).resolves.toBeInTheDocument(); + await expect(screen.findByText('Wrapper#0 Y')).resolves.toBeInTheDocument(); + + await userEvent.click(screen.getAllByText('Increment')[0]); + + await expect(screen.findByText('Wrapper#1 X')).resolves.toBeInTheDocument(); + await expect(screen.findByText('Wrapper#1 Y')).resolves.toBeInTheDocument(); + + await userEvent.click(screen.getAllByText('Increment')[1]); + + await expect(screen.findByText('Wrapper#2 X')).resolves.toBeInTheDocument(); + await expect(screen.findByText('Wrapper#2 Y')).resolves.toBeInTheDocument(); + }); + + it('should not rerender adjacent hooks on update', async () => { + let renderCountA = 0; + let renderCountB = 0; + + const api = DefaultPluginWrapperApi.fromWrappers([ + { + loader: async (): Promise> => ({ + component: makeTestIncWrapper('A', () => { + renderCountA += 1; + }), + useWrapperValue: useTestInc, + }), + pluginId: 'plugin-a', + }, + { + loader: async (): Promise> => ({ + component: makeTestIncWrapper('B', () => { + renderCountB += 1; + }), + useWrapperValue: useTestInc, + }), + pluginId: 'plugin-b', + }, + ]); + + const WrapperA = api.getPluginWrapper('plugin-a')!; + const WrapperB = api.getPluginWrapper('plugin-b')!; + + expect(WrapperA).toBeDefined(); + expect(WrapperB).toBeDefined(); + + const RootWrapper = api.getRootWrapper(); + + render( + + X + Y + , + ); + + await expect( + screen.findByText('WrapperA#0 X'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByText('WrapperB#0 Y'), + ).resolves.toBeInTheDocument(); + + expect(renderCountA).toBe(1); + expect(renderCountB).toBe(1); + + await userEvent.click(screen.getByText('IncrementA')); + + expect(screen.getByText('WrapperA#1 X')).toBeInTheDocument(); + expect(screen.getByText('WrapperB#0 Y')).toBeInTheDocument(); + + expect(renderCountA).toBe(2); + expect(renderCountB).toBe(1); + + await userEvent.click(screen.getByText('IncrementB')); + + expect(screen.getByText('WrapperA#1 X')).toBeInTheDocument(); + expect(screen.getByText('WrapperB#1 Y')).toBeInTheDocument(); + + expect(renderCountA).toBe(2); + expect(renderCountB).toBe(2); + }); }); diff --git a/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx b/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx index 7e602ee23f..1c6037a6f3 100644 --- a/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx +++ b/plugins/app/src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx @@ -14,11 +14,36 @@ * limitations under the License. */ -import { PluginWrapperApi } from '@backstage/frontend-plugin-api/alpha'; -import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react'; +import { + PluginWrapperApi, + PluginWrapperDefinition, +} from '@backstage/frontend-plugin-api'; +import { + ComponentType, + ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useState, + useSyncExternalStore, +} from 'react'; + +interface HookStore { + getSnapshot: () => { value: unknown } | undefined; + subscribe: (listener: () => void) => () => void; +} + +interface HookRegistryContextValue { + registerHook: (key: any, hook: () => unknown) => HookStore; +} + +const HookRegistryContext = createContext( + undefined, +); type WrapperInput = { - loader: () => Promise<{ component: ComponentType<{ children: ReactNode }> }>; + loader: () => Promise>; pluginId: string; }; @@ -29,12 +54,17 @@ type WrapperInput = { */ export class DefaultPluginWrapperApi implements PluginWrapperApi { constructor( + private readonly rootWrapper: ComponentType<{ children: ReactNode }>, private readonly pluginWrappers: Map< string, ComponentType<{ children: ReactNode }> >, ) {} + getRootWrapper(): ComponentType<{ children: ReactNode }> { + return this.rootWrapper; + } + getPluginWrapper( pluginId: string, ): ComponentType<{ children: ReactNode }> | undefined { @@ -44,9 +74,7 @@ export class DefaultPluginWrapperApi implements PluginWrapperApi { static fromWrappers(wrappers: Array): DefaultPluginWrapperApi { const loadersByPlugin = new Map< string, - Array< - () => Promise<{ component: ComponentType<{ children: ReactNode }> }> - > + Array<() => Promise>> >(); for (const wrapper of wrappers) { @@ -68,6 +96,45 @@ export class DefaultPluginWrapperApi implements PluginWrapperApi { continue; } + const WrapperWithState = ({ + loader, + component: WrapperComponent, + useWrapperValue, + children, + }: { + loader: () => Promise; + component: ComponentType<{ + children: ReactNode; + value: unknown; + }>; + useWrapperValue: () => unknown; + children: ReactNode; + }) => { + const hookContext = useContext(HookRegistryContext); + if (!hookContext) { + throw new Error( + 'Attempted to render a wrapped plugin component without a root wrapper context', + ); + } + const store = useMemo(() => { + return hookContext.registerHook(loader, useWrapperValue); + }, [hookContext, loader, useWrapperValue]); + const container = useSyncExternalStore( + store.subscribe, + store.getSnapshot, + ); + + if (!container) { + return null; + } + + return ( + + {children} + + ); + }; + const ComposedWrapper = (props: { children: ReactNode }) => { const [loadedWrappers, setLoadedWrappers] = useState< Array> | undefined @@ -77,7 +144,27 @@ export class DefaultPluginWrapperApi implements PluginWrapperApi { useEffect(() => { Promise.all(loaders.map(loader => loader())) .then(results => { - setLoadedWrappers(results.map(r => r.component)); + const normalizedResults = results.map( + ({ component, useWrapperValue }, index) => { + const loader = loaders[index]; + + if (!useWrapperValue) { + return component as ComponentType<{ children: ReactNode }>; + } + + return ({ children }: { children: ReactNode }) => ( + + {children} + + ); + }, + ); + + setLoadedWrappers(normalizedResults); }) .catch(setError); }, []); @@ -86,24 +173,107 @@ export class DefaultPluginWrapperApi implements PluginWrapperApi { throw error; } - return useMemo(() => { - if (!loadedWrappers) { - return null; - } + if (!loadedWrappers) { + return null; + } - let current = props.children; + let content = props.children; - for (const Wrapper of loadedWrappers) { - current = {current}; - } + for (const Wrapper of loadedWrappers) { + content = {content}; + } - return current; - }, [loadedWrappers, props.children]); + return <>{content}; }; composedWrappers.set(pluginId, ComposedWrapper); } - return new DefaultPluginWrapperApi(composedWrappers); + return new DefaultPluginWrapperApi( + DefaultPluginWrapperApi.createRootWrapper(), + composedWrappers, + ); + } + + /** + * Creates the root wrapper component that is responsible for rendering and + * forwarding the values of the common `useWrapperValue` hooks. + */ + static createRootWrapper() { + const renderers = new Map(); + const renderUpdateListeners = new Set<() => void>(); + + let renderElements = new Array(); + + const createHookRenderer = (hook: () => unknown): HookStore => { + const listeners = new Set<() => void>(); + let container: { value: unknown } | undefined = undefined; + + const HookRenderer = () => { + container = { value: hook() }; + useEffect(() => { + for (const listener of listeners) { + listener(); + } + }); + return null; + }; + + renderElements = [ + ...renderElements, + , + ]; + + return { + getSnapshot: () => container, + subscribe(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }; + }; + + const registerHook = (key: any, hook: () => unknown) => { + let renderer = renderers.get(key); + if (!renderer) { + renderer = createHookRenderer(hook); + renderers.set(key, renderer); + + queueMicrotask(() => { + for (const listener of renderUpdateListeners) { + listener(); + } + }); + } + return renderer; + }; + + const subscribeToRenderUpdates = (listener: () => void) => { + renderUpdateListeners.add(listener); + return () => renderUpdateListeners.delete(listener); + }; + const getRenderElements = () => renderElements; + + const RootWrapper = (props: { children: ReactNode }) => { + const elements = useSyncExternalStore( + subscribeToRenderUpdates, + getRenderElements, + ); + + return ( + <> + <>{elements} + + {props.children} + + + ); + }; + + return RootWrapper; } } diff --git a/plugins/app/src/extensions/AppLanguageApi.ts b/plugins/app/src/extensions/AppLanguageApi.ts index 4f605aa30f..80efa1d9e9 100644 --- a/plugins/app/src/extensions/AppLanguageApi.ts +++ b/plugins/app/src/extensions/AppLanguageApi.ts @@ -16,7 +16,7 @@ // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppLanguageSelector } from '../../../../packages/core-app-api/src/apis/implementations/AppLanguageApi'; -import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha'; +import { appLanguageApiRef } from '@backstage/frontend-plugin-api'; import { ApiBlueprint } from '@backstage/frontend-plugin-api'; export const AppLanguageApi = ApiBlueprint.makeWithOverrides({ diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index 12e0c1a5c8..129a94cd9e 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -20,55 +20,117 @@ import { createExtensionInput, NavItemBlueprint, routeResolutionApiRef, + appTreeApiRef, IconComponent, + IconElement, RouteRef, + RouteResolutionApi, useApi, } from '@backstage/frontend-plugin-api'; import { NavContentBlueprint, NavContentComponent, NavContentComponentProps, + NavContentNavItem, + NavContentNavItems, } from '@backstage/plugin-app-react'; import { Sidebar, SidebarItem } from '@backstage/core-components'; import { useMemo } from 'react'; +class NavItemBag implements NavContentNavItems { + readonly #items: NavContentNavItem[]; + readonly #index: Map; + readonly #taken: Set; + + constructor(items: NavContentNavItem[], taken?: Iterable) { + this.#items = items; + this.#index = new Map(items.map(item => [item.node.spec.id, item])); + this.#taken = new Set(taken); + } + + take(id: string): NavContentNavItem | undefined { + const item = this.#index.get(id); + if (item) { + this.#taken.add(id); + } + return item; + } + + rest(): NavContentNavItem[] { + return this.#items.filter(item => !this.#taken.has(item.node.spec.id)); + } + + clone(): NavContentNavItems { + return new NavItemBag(this.#items, this.#taken); + } + + withComponent(Component: (props: NavContentNavItem) => JSX.Element) { + return { + take: (id: string) => { + const item = this.take(id); + return item ? : null; + }, + rest: (options?: { sortBy?: 'title' }) => { + const items = this.rest(); + if (options?.sortBy === 'title') { + items.sort((a, b) => a.title.localeCompare(b.title)); + } + return items.map(item => ( + + )); + }, + }; + } +} + function DefaultNavContent(props: NavContentComponentProps) { + const items = props.navItems.rest(); return ( - {props.items.map((item, index) => ( + {items.map(item => ( item.icon} + text={item.title} + key={item.node.spec.id} /> ))} ); } -// This helps defer rendering until the app is being rendered, which is needed -// because the RouteResolutionApi can't be called until the app has been fully initialized. +// Tries to resolve a routeRef to a link path, returning undefined if it +// can't be resolved (e.g. parameterized routes). +function tryResolveLink( + routeResolutionApi: RouteResolutionApi, + routeRef: RouteRef, +): string | undefined { + try { + const link = routeResolutionApi.resolve(routeRef); + return link?.(); + } catch { + return undefined; + } +} + +// Defers rendering until the app is fully initialized so that APIs like +// RouteResolutionApi and AppTreeApi are available. function NavContentRenderer(props: { Content: NavContentComponent; - items: Array<{ + legacyNavItems: Array<{ title: string; icon: IconComponent; routeRef: RouteRef; }>; }) { + const appTreeApi = useApi(appTreeApiRef); const routeResolutionApi = useApi(routeResolutionApiRef); - const items = useMemo(() => { - return props.items.flatMap(item => { + // Deprecated items: just resolve nav item routeRefs to paths, no page discovery. + const legacyItems = useMemo(() => { + return props.legacyNavItems.flatMap(item => { const link = routeResolutionApi.resolve(item.routeRef); - if (!link) { - // eslint-disable-next-line no-console - console.warn( - `NavItemBlueprint: unable to resolve route ref ${item.routeRef}`, - ); - return []; - } + if (!link) return []; return [ { to: link(), @@ -79,9 +141,80 @@ function NavContentRenderer(props: { }, ]; }); - }, [props.items, routeResolutionApi]); + }, [props.legacyNavItems, routeResolutionApi]); - return ; + // New navItems: discover pages from the extension tree, merged with nav items. + const navItems = useMemo(() => { + const { tree } = appTreeApi.getTree(); + const routesNode = tree.nodes.get('app/routes'); + if (!routesNode) return new NavItemBag([]); + + // Index nav items by routeRef for matching against pages + const navItemsByRouteRef = new Map< + RouteRef, + { title: string; icon: IconComponent } + >(props.legacyNavItems.map(item => [item.routeRef, item])); + + const pageNodes = routesNode.edges.attachments.get('routes') ?? []; + const items = pageNodes.flatMap((node): NavContentNavItem[] => { + if (!node.instance || node.spec.disabled) { + return []; + } + + const routeRef = node.instance.getData(coreExtensionData.routeRef); + if (!routeRef) { + return []; + } + + const matchingNavItem = navItemsByRouteRef.get(routeRef); + + // PageBlueprint resolves title as: config.title ?? params.title ?? plugin.title ?? pluginId + // We want the priority: page (config/params) -> nav item -> plugin -> pluginId + const resolvedTitle = node.instance.getData(coreExtensionData.title); + const pluginTitle = node.spec.plugin.title; + const pluginIcon = node.spec.plugin.icon; + const pluginId = node.spec.plugin.pluginId; + const hasExplicitPageTitle = + resolvedTitle !== undefined && + resolvedTitle !== pluginTitle && + resolvedTitle !== pluginId; + const title = hasExplicitPageTitle + ? resolvedTitle + : matchingNavItem?.title ?? pluginTitle ?? pluginId; + + // PageBlueprint resolves icon as: params.icon ?? plugin.icon + // We want the priority: page (params) -> nav item -> plugin -> (excluded) + const resolvedIcon = node.instance.getData(coreExtensionData.icon); + const hasExplicitPageIcon = resolvedIcon && !node.spec.plugin.icon; + const NavItemIcon = matchingNavItem?.icon; + + let icon: IconElement | undefined; + if (hasExplicitPageIcon) { + icon = resolvedIcon; + } else if (NavItemIcon) { + icon = ; + } else if (resolvedIcon) { + icon = resolvedIcon; + } else if (pluginIcon) { + icon = pluginIcon; + } + + if (!title || !icon) { + return []; + } + + const to = tryResolveLink(routeResolutionApi, routeRef); + if (!to) { + return []; + } + + return [{ node, href: to, title, icon, routeRef }]; + }); + + return new NavItemBag(items); + }, [appTreeApi, routeResolutionApi, props.legacyNavItems]); + + return ; } export const AppNav = createExtension({ @@ -103,7 +236,7 @@ export const AppNav = createExtension({ yield coreExtensionData.reactElement( + legacyNavItems={inputs.items.map(item => item.get(NavItemBlueprint.dataRefs.target), )} Content={Content} diff --git a/plugins/app/src/extensions/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index 08eaa50bd9..9ae564e06d 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -29,12 +29,15 @@ import { createExtension, createExtensionInput, routeResolutionApiRef, + pluginWrapperApiRef, + useAnalytics, } from '@backstage/frontend-plugin-api'; import { AppRootWrapperBlueprint, RouterBlueprint, SignInPageBlueprint, } from '@backstage/plugin-app-react'; +import { BUIProvider } from '@backstage/ui'; import { DiscoveryApi, ErrorApi, @@ -113,6 +116,12 @@ export const AppRoot = createExtension({ } } + const pluginWrapperApi = apis.get(pluginWrapperApiRef); + const RootWrapper = pluginWrapperApi?.getRootWrapper(); + if (RootWrapper) { + content = {content}; + } + return [ coreExtensionData.reactElement( - {...extraElements} - - {children} + + {...extraElements} + + {children} + ); } return ( - {...extraElements} - - - {children} - + + {...extraElements} + + + {children} + + ); } diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.tsx similarity index 100% rename from plugins/app/src/extensions/IconsApi.ts rename to plugins/app/src/extensions/IconsApi.tsx diff --git a/plugins/app/src/extensions/PluginHeaderActionsApi.ts b/plugins/app/src/extensions/PluginHeaderActionsApi.ts new file mode 100644 index 0000000000..58c67e239a --- /dev/null +++ b/plugins/app/src/extensions/PluginHeaderActionsApi.ts @@ -0,0 +1,49 @@ +/* + * 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 { + coreExtensionData, + pluginHeaderActionsApiRef, + createExtensionInput, + ApiBlueprint, +} from '@backstage/frontend-plugin-api'; +import { DefaultPluginHeaderActionsApi } from '../apis/PluginHeaderActionsApi'; + +/** + * Contains the plugin-scoped header actions installed into the app. + */ +export const PluginHeaderActionsApi = ApiBlueprint.makeWithOverrides({ + name: 'plugin-header-actions', + inputs: { + actions: createExtensionInput([coreExtensionData.reactElement]), + }, + factory: (originalFactory, { inputs }) => { + return originalFactory(defineParams => + defineParams({ + api: pluginHeaderActionsApiRef, + deps: {}, + factory: () => { + return DefaultPluginHeaderActionsApi.fromActions( + inputs.actions.map(actionInput => ({ + element: actionInput.get(coreExtensionData.reactElement), + pluginId: actionInput.node.spec.plugin.pluginId, + })), + ); + }, + }), + ); + }, +}); diff --git a/plugins/app/src/extensions/PluginWrapperApi.ts b/plugins/app/src/extensions/PluginWrapperApi.ts index e402f094f1..836f563fee 100644 --- a/plugins/app/src/extensions/PluginWrapperApi.ts +++ b/plugins/app/src/extensions/PluginWrapperApi.ts @@ -17,8 +17,6 @@ import { PluginWrapperBlueprint, pluginWrapperApiRef, -} from '@backstage/frontend-plugin-api/alpha'; -import { createExtensionInput, ApiBlueprint, } from '@backstage/frontend-plugin-api'; diff --git a/plugins/app/src/extensions/TranslationsApi.tsx b/plugins/app/src/extensions/TranslationsApi.tsx index a327aba299..c7f259d135 100644 --- a/plugins/app/src/extensions/TranslationsApi.tsx +++ b/plugins/app/src/extensions/TranslationsApi.tsx @@ -21,7 +21,7 @@ import { TranslationBlueprint } from '@backstage/plugin-app-react'; import { appLanguageApiRef, translationApiRef, -} from '@backstage/core-plugin-api/alpha'; +} from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { I18nextTranslationApi } from '../../../../packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi'; diff --git a/plugins/app/src/extensions/components.tsx b/plugins/app/src/extensions/components.tsx index 72a77e2446..230db89767 100644 --- a/plugins/app/src/extensions/components.tsx +++ b/plugins/app/src/extensions/components.tsx @@ -17,6 +17,8 @@ import { NotFoundErrorPage as SwappableNotFoundErrorPage, Progress as SwappableProgress, ErrorDisplay as SwappableErrorDisplay, + PageLayout as SwappablePageLayout, + type PageLayoutProps, } from '@backstage/frontend-plugin-api'; import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { @@ -24,7 +26,9 @@ import { ErrorPanel, Progress as ProgressComponent, } from '@backstage/core-components'; +import { PluginHeader } from '@backstage/ui'; import Button from '@material-ui/core/Button'; +import { useMemo } from 'react'; export const Progress = SwappableComponentBlueprint.make({ name: 'core-progress', @@ -63,3 +67,39 @@ export const ErrorDisplay = SwappableComponentBlueprint.make({ }, }), }); + +export const PageLayout = SwappableComponentBlueprint.make({ + name: 'core-page-layout', + params: define => + define({ + component: SwappablePageLayout, + loader: () => (props: PageLayoutProps) => { + const { title, icon, noHeader, headerActions, tabs, children } = props; + const tabsWithMatchStrategy = useMemo( + () => + tabs?.map(tab => ({ + ...tab, + matchStrategy: 'prefix' as const, + })), + [tabs], + ); + + if (tabsWithMatchStrategy) { + return ( + <> + {!noHeader && ( + + )} + {children} + + ); + } + return <>{children}; + }, + }), +}); diff --git a/plugins/app/src/extensions/index.ts b/plugins/app/src/extensions/index.ts index 17ac19ff4b..d9e2b666f1 100644 --- a/plugins/app/src/extensions/index.ts +++ b/plugins/app/src/extensions/index.ts @@ -31,5 +31,11 @@ export { oauthRequestDialogAppRootElement, alertDisplayAppRootElement, } from './elements'; -export { Progress, NotFoundErrorPage, ErrorDisplay } from './components'; +export { + Progress, + NotFoundErrorPage, + ErrorDisplay, + PageLayout, +} from './components'; export { PluginWrapperApi } from './PluginWrapperApi'; +export { PluginHeaderActionsApi } from './PluginHeaderActionsApi'; diff --git a/plugins/app/src/plugin.ts b/plugins/app/src/plugin.ts index ec0c627f47..8d2fd56720 100644 --- a/plugins/app/src/plugin.ts +++ b/plugins/app/src/plugin.ts @@ -29,6 +29,7 @@ import { IconsApi, FeatureFlagsApi, PluginWrapperApi, + PluginHeaderActionsApi, TranslationsApi, oauthRequestDialogAppRootElement, alertDisplayAppRootElement, @@ -37,6 +38,7 @@ import { Progress, NotFoundErrorPage, ErrorDisplay, + PageLayout, LegacyComponentsApi, } from './extensions'; import { apis } from './defaultApis'; @@ -60,6 +62,7 @@ export const appPlugin = createFrontendPlugin({ IconsApi, FeatureFlagsApi, PluginWrapperApi, + PluginHeaderActionsApi, TranslationsApi, DefaultSignInPage, oauthRequestDialogAppRootElement, @@ -68,6 +71,7 @@ export const appPlugin = createFrontendPlugin({ Progress, NotFoundErrorPage, ErrorDisplay, + PageLayout, LegacyComponentsApi, ], }); diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index e22b2632ff..54e309818e 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index b1a92da282..e96be47580 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.4.12-next.0", + "version": "0.4.13-next.1", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md index 132e1941fc..5f55a061d7 100644 --- a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-auth-backend-module-auth0-provider +## 0.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.0 + +### Minor Changes + +- 36804fe: feat: Added organization option to authorization params of the strategy + +### Patch Changes + +- 867c905: Add support for organizational invites in auth0 strategy +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.0-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index e5f53f929e..c3caacd61d 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-auth0-provider", - "version": "0.3.0-next.1", + "version": "0.3.1-next.1", "description": "The auth0-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index ba8e4ee07d..ab78cfb6a1 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.4.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.13-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 1276db1d22..7145eea19f 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.4.13-next.1", + "version": "0.4.14-next.1", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts index 73f859df52..e7e8fb96a5 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts @@ -136,14 +136,15 @@ describe('AwsAlbProvider', () => { }); it('JWT is invalid', async () => { - await expect( - awsAlbAuthenticator.authenticate( + const err = await awsAlbAuthenticator + .authenticate( { req: mockRequestWithInvalidJwt }, { issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn() }, - ), - ).rejects.toThrow( - 'Exception occurred during JWT processing: JWSInvalid: Invalid Compact JWS', - ); + ) + .catch(e => e); + expect(err).toBeInstanceOf(AuthenticationError); + expect(err.message).toContain('Exception occurred during JWT processing'); + expect(err.cause).toBeDefined(); }); it('Email is missing', async () => { @@ -160,18 +161,19 @@ describe('AwsAlbProvider', () => { return undefined; }), } as unknown as express.Request; - await expect( - awsAlbAuthenticator.authenticate( + + const err = await awsAlbAuthenticator + .authenticate( { req }, { issuer: 'ISSUER_URL', signer: undefined, getKey: jest.fn().mockResolvedValue(signingKey), }, - ), - ).rejects.toThrow( - 'Exception occurred during JWT processing: AuthenticationError: Missing email in the JWT token', - ); + ) + .catch(e => e); + expect(err).toBeInstanceOf(AuthenticationError); + expect(err.message).toContain('Missing email in the JWT token'); }); it('issuer is missing', async () => { @@ -189,18 +191,18 @@ describe('AwsAlbProvider', () => { }), } as unknown as express.Request; - await expect( - awsAlbAuthenticator.authenticate( + const err = await awsAlbAuthenticator + .authenticate( { req }, { issuer: 'ISSUER_URL', signer: undefined, getKey: jest.fn().mockResolvedValue(signingKey), }, - ), - ).rejects.toThrow( - 'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token', - ); + ) + .catch(e => e); + expect(err).toBeInstanceOf(AuthenticationError); + expect(err.message).toContain('Issuer mismatch on JWT token'); }); it('issuer is invalid', async () => { @@ -218,18 +220,18 @@ describe('AwsAlbProvider', () => { }), } as unknown as express.Request; - await expect( - awsAlbAuthenticator.authenticate( + const err = await awsAlbAuthenticator + .authenticate( { req }, { issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn().mockResolvedValue(signingKey), }, - ), - ).rejects.toThrow( - 'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token', - ); + ) + .catch(e => e); + expect(err).toBeInstanceOf(AuthenticationError); + expect(err.message).toContain('Issuer mismatch on JWT token'); }); it('signer is invalid', async () => { @@ -247,18 +249,18 @@ describe('AwsAlbProvider', () => { }), } as unknown as express.Request; - await expect( - awsAlbAuthenticator.authenticate( + const err = await awsAlbAuthenticator + .authenticate( { req }, { issuer: 'ISSUER_URL', signer: 'SIGNER_ARN', getKey: jest.fn().mockResolvedValue(signingKey), }, - ), - ).rejects.toThrow( - 'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token', - ); + ) + .catch(e => e); + expect(err).toBeInstanceOf(AuthenticationError); + expect(err.message).toContain('Issuer mismatch on JWT token'); }); }); diff --git a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts index b6969c5432..5f7eb62219 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts @@ -98,7 +98,13 @@ export const awsAlbAuthenticator = createProxyAuthenticator({ }, }; } catch (e) { - throw new Error(`Exception occurred during JWT processing: ${e}`); + if (e.name === 'AuthenticationError') { + throw e; + } + throw new AuthenticationError( + 'Exception occurred during JWT processing', + e, + ); } }, }); diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index e243dc4447..7e2524ce0e 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.2.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.2.16-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 044c994d00..fcb2c85d97 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.2.17-next.0", + "version": "0.2.18-next.1", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index c5cdf2e4fb..099b827bdc 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index d985022df9..4835828f1a 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.3.12-next.0", + "version": "0.3.13-next.1", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md index 5c07ba85d2..f1d80afeea 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-bitbucket-server-provider +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.2.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.2.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index 051aac9b1f..4ae6ff37eb 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-server-provider", - "version": "0.2.12-next.0", + "version": "0.2.13-next.1", "description": "The bitbucket-server-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index e3c22301d7..742cf354f0 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 4f5e6d7411..60f6cbc413 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.4.12-next.0", + "version": "0.4.13-next.1", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 0e9511768c..c9fe1157ba 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 8ef0f46c39..2c29c29e3c 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.4.12-next.0", + "version": "0.4.13-next.1", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index f3deb2236a..8f0c8ef22e 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.5.0 + +### Minor Changes + +- ff07934: Added the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by their GitHub user ID. + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.5.0-next.0 ### Minor Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 16ea13bc0c..bcebf762fd 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.5.0-next.0", + "version": "0.5.1-next.1", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index ac0bd2a400..c1ccca2bc7 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.0 + +### Minor Changes + +- ff07934: Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.0-next.0 ### Minor Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 6e83326663..8a12e491d7 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.4.0-next.0", + "version": "0.4.1-next.1", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 8d044fd62d..40e083908d 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index c4ff9ab5e1..fa0ae5e42d 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.3.12-next.0", + "version": "0.3.13-next.1", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index 3fe09f2860..609272b1cd 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.2.16-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 87d300773e..24f11cff0a 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.2.16-next.0", + "version": "0.2.17-next.1", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 5ae6242297..1a5d6f5c41 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 6cebe04541..a869d75af2 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.3.12-next.0", + "version": "0.3.13-next.1", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 21b13ccb29..2f81b21928 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index c95823699e..6eb517fbc6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.4.12-next.0", + "version": "0.4.13-next.1", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index 452bad8707..777b862c71 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.2.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.2.16-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index 4cc5be1815..caece3cfb2 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.2.17-next.0", + "version": "0.2.18-next.1", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index 938aa81079..8e900f1ccf 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.4.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.2 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.4.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.4.13-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 94af72df96..85f9b81cdd 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.4.13-next.1", + "version": "0.4.14-next.1", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index 98070b4475..9d40e0bc9a 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.2.12 + +### Patch Changes + +- 08aea95: Added a validation check that rejects `audience` configuration values that are not absolute URLs (i.e. missing `https://` or `http://` prefix). +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.2.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index c3acab1d1d..16b05d8f60 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.2.12-next.0", + "version": "0.2.13-next.1", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index 068b6a58ff..a26d1b4145 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.12 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 25c725a50d..4d9258a5df 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.3.12-next.0", + "version": "0.3.13-next.1", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-openshift-provider/CHANGELOG.md b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md index 25b1e38b4b..434ad479d9 100644 --- a/plugins/auth-backend-module-openshift-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-auth-backend-module-openshift-provider +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json index a749867d5c..4191cf7bc7 100644 --- a/plugins/auth-backend-module-openshift-provider/package.json +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-openshift-provider", - "version": "0.1.4-next.0", + "version": "0.1.5-next.1", "description": "The OpenShift backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index bac5a7c2fd..2f1dd9ff99 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.3.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index a2179e5973..34ad26fb11 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index 31a7c84dda..c83c7dae4a 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index e577dfc45a..b2b195d72b 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.5.11-next.0", + "version": "0.5.12-next.1", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 56daba0988..d771505212 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/plugin-auth-backend +## 0.27.1-next.2 + +### Patch Changes + +- d0f4cd2: Added optional client metadata document endpoint at `/.well-known/oauth-client/cli.json` relative to the auth backend base URL for CLI authentication. Enabled when `auth.experimentalClientIdMetadataDocuments.enabled` is set to `true`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.27.1-next.1 + +### Patch Changes + +- 1ccad86: Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.27.1-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 619be54: Update migrations to be reversible +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## 0.27.0 + +### Minor Changes + +- 31de2c9: Added experimental support for Client ID Metadata Documents (CIMD). + + This allows Backstage to act as an OAuth 2.0 authorization server that supports the [IETF Client ID Metadata Document draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). External OAuth clients can use HTTPS URLs as their `client_id`, and Backstage will fetch metadata from those URLs to validate the client. + + **Configuration example:** + + ```yaml + auth: + experimentalClientIdMetadataDocuments: + enabled: true + # Optional: restrict which `client_id` URLs are allowed (defaults to ['*']) + allowedClientIdPatterns: + - 'https://example.com/*' + - 'https://*.trusted-domain.com/*' + # Optional: restrict which redirect URIs are allowed (defaults to ['*']) + allowedRedirectUriPatterns: + - 'http://localhost:*' + - 'https://*.example.com/*' + ``` + + Clients using CIMD must host a JSON metadata document at their `client_id` URL containing at minimum: + + ```json + { + "client_id": "https://example.com/.well-known/oauth-client/my-app", + "client_name": "My Application", + "redirect_uris": ["http://localhost:8080/callback"], + "token_endpoint_auth_method": "none" + } + ``` + +- d0786b9: Added experimental support for refresh tokens via the `auth.experimentalRefreshToken.enabled` configuration option. When enabled, clients can request the `offline_access` scope to receive refresh tokens that can be used to obtain new access tokens without re-authentication. + +### Patch Changes + +- 7dc3dfe: Removed the `auth.experimentalDynamicClientRegistration.tokenExpiration` config option. DCR tokens now use the default 1 hour expiration. + + If you need longer-lived access, use refresh tokens via the `offline_access` scope instead. DCR clients should already have the `offline_access` scope available. Enable refresh tokens by setting: + + ```yaml + auth: + experimentalRefreshToken: + enabled: true + ``` + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + ## 0.27.0-next.1 ### Minor Changes diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 0dd531c34e..9326394a2a 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -95,7 +95,6 @@ export interface Config { /** * The backstage token expiration. - * Defaults to 1 hour (3600s). Maximum allowed is 24 hours. */ backstageTokenExpiration?: HumanDuration | string; @@ -150,12 +149,37 @@ export interface Config { * dynamic client registration. Defaults to '[*]' which allows any redirect URI. */ allowedRedirectUriPatterns?: string[]; + }; + + /** + * Configuration for Client ID Metadata Documents (CIMD) + * + * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ + */ + experimentalClientIdMetadataDocuments?: { + /** + * Whether to enable Client ID Metadata Documents support + * Defaults to false + */ + enabled?: boolean; /** - * The expiration time for the client registration access tokens. - * Defaults to 1 hour (3600s). Maximum allowed is 24 hours. + * A list of allowed URI patterns for client_id URLs. + * Uses glob-style pattern matching where `*` matches any characters. + * Defaults to ['*'] which allows any client_id URL. + * + * @example ['https://example.com/*', 'https://*.trusted-domain.com/*'] */ - tokenExpiration?: HumanDuration | string; + allowedClientIdPatterns?: string[]; + + /** + * A list of allowed URI patterns for redirect URIs. + * Uses glob-style pattern matching where `*` matches any characters. + * Defaults to ['*'] which allows any redirect URI. + * + * @example ['http://localhost:*', 'http://127.0.0.1:*\/callback'] + */ + allowedRedirectUriPatterns?: string[]; }; }; } diff --git a/plugins/auth-backend/migrations/20200619125845_init.js b/plugins/auth-backend/migrations/20200619125845_init.js index d5fc08ce48..a53ac46ea9 100644 --- a/plugins/auth-backend/migrations/20200619125845_init.js +++ b/plugins/auth-backend/migrations/20200619125845_init.js @@ -42,5 +42,5 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - return knex.schema.dropTable('auth_keystore'); + return knex.schema.dropTable('signing_keys'); }; diff --git a/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js b/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js index ab7e713b96..86a87ebe7c 100644 --- a/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js +++ b/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js @@ -48,7 +48,7 @@ exports.down = async function down(knex) { if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('signing_keys', table => { table - .timestamp('created_at', { useTz: false, precision: 0 }) + .timestamp('created_at', { useTz: true, precision: 0 }) .notNullable() .defaultTo(knex.fn.now()) .comment('The creation time of the key') diff --git a/plugins/auth-backend/migrations/20251217120000_drop_oidc_clients_fk.js b/plugins/auth-backend/migrations/20251217120000_drop_oidc_clients_fk.js new file mode 100644 index 0000000000..f08032269d --- /dev/null +++ b/plugins/auth-backend/migrations/20251217120000_drop_oidc_clients_fk.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +// @ts-check + +/** + * Drop the foreign key constraint on oauth_authorization_sessions.client_id + * to allow CIMD (Client ID Metadata Document) clients which are not stored + * in the oidc_clients table. + * + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('oauth_authorization_sessions', table => { + table.dropForeign(['client_id']); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + // Delete sessions with CIMD client_ids (not in oidc_clients) before re-adding FK + await knex('oauth_authorization_sessions') + .whereNotIn('client_id', knex('oidc_clients').select('client_id')) + .delete(); + + await knex.schema.alterTable('oauth_authorization_sessions', table => { + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + }); +}; diff --git a/plugins/auth-backend/migrations/20260317120000_drop_offline_sessions_fk.js b/plugins/auth-backend/migrations/20260317120000_drop_offline_sessions_fk.js new file mode 100644 index 0000000000..d67f069950 --- /dev/null +++ b/plugins/auth-backend/migrations/20260317120000_drop_offline_sessions_fk.js @@ -0,0 +1,49 @@ +/* + * Copyright 2026 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. + */ + +// @ts-check + +/** + * Drop the foreign key constraint on offline_sessions.oidc_client_id + * to allow CIMD (Client ID Metadata Document) clients which are not stored + * in the oidc_clients table. + * + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('offline_sessions', table => { + table.dropForeign(['oidc_client_id']); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + // Delete sessions with CIMD client_ids (not in oidc_clients) before re-adding FK + await knex('offline_sessions') + .whereNotNull('oidc_client_id') + .whereNotIn('oidc_client_id', knex('oidc_clients').select('client_id')) + .delete(); + + await knex.schema.alterTable('offline_sessions', table => { + table + .foreign('oidc_client_id') + .references('client_id') + .inTable('oidc_clients') + .onDelete('CASCADE'); + }); +}; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index cb0ce48a86..c4fdee3338 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.27.0-next.1", + "version": "0.27.1-next.2", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", @@ -57,12 +57,13 @@ "express": "^4.22.0", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", + "ipaddr.js": "^2.3.0", "jose": "^5.0.0", "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", "matcher": "^4.0.0", - "minimatch": "^9.0.0", + "minimatch": "^10.2.1", "passport": "^0.7.0", "uuid": "^11.0.0", "zod": "^4.3.5", @@ -78,6 +79,7 @@ "@types/express": "^4.17.6", "@types/express-session": "^1.17.2", "@types/passport": "^1.0.3", + "msw": "^1.0.0", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/auth-backend/report.sql.md b/plugins/auth-backend/report.sql.md index 4dc160a2bf..2da5473734 100644 --- a/plugins/auth-backend/report.sql.md +++ b/plugins/auth-backend/report.sql.md @@ -2,9 +2,6 @@ > Do not edit this file. It is a report generated by `yarn build:api-reports` -> [!WARNING] -> Failed to migrate down from '20220321100910_timestamptz_again.js' - ## Table `oauth_authorization_sessions` | Column | Type | Nullable | Max Length | Default | diff --git a/plugins/auth-backend/src/actions/createWhoAmIAction.test.ts b/plugins/auth-backend/src/actions/createWhoAmIAction.test.ts new file mode 100644 index 0000000000..6de064e3e9 --- /dev/null +++ b/plugins/auth-backend/src/actions/createWhoAmIAction.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { createWhoAmIAction } from './createWhoAmIAction'; + +const mockUserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'mock', + namespace: 'default', + }, + spec: { + profile: { + displayName: 'Mock User', + email: 'mock@example.com', + }, + }, +}; + +describe('createWhoAmIAction', () => { + it('should return the user entity and user info for authenticated user credentials', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + + createWhoAmIAction({ + auth: mockServices.auth(), + catalog: catalogServiceMock({ entities: [mockUserEntity] }), + userInfo: mockServices.userInfo(), + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:who-am-i', + input: {}, + credentials: mockCredentials.user(), + }); + + expect(result.output).toEqual({ + entity: mockUserEntity, + userInfo: { + userEntityRef: 'user:default/mock', + ownershipEntityRefs: ['user:default/mock'], + }, + }); + }); + + it('should throw when called with service credentials', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + + createWhoAmIAction({ + auth: mockServices.auth(), + catalog: catalogServiceMock({ entities: [mockUserEntity] }), + userInfo: mockServices.userInfo(), + actionsRegistry: mockActionsRegistry, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:who-am-i', + input: {}, + credentials: mockCredentials.service(), + }), + ).rejects.toThrow('This action requires user credentials'); + }); + + it('should throw when the user entity is not found in the catalog', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + + createWhoAmIAction({ + auth: mockServices.auth(), + catalog: catalogServiceMock(), + userInfo: mockServices.userInfo(), + actionsRegistry: mockActionsRegistry, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:who-am-i', + input: {}, + credentials: mockCredentials.user(), + }), + ).rejects.toThrow( + 'User entity not found in the catalog for "user:default/mock"', + ); + }); +}); diff --git a/plugins/auth-backend/src/actions/createWhoAmIAction.ts b/plugins/auth-backend/src/actions/createWhoAmIAction.ts new file mode 100644 index 0000000000..2a9817bf57 --- /dev/null +++ b/plugins/auth-backend/src/actions/createWhoAmIAction.ts @@ -0,0 +1,92 @@ +/* + * 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 { AuthService, UserInfoService } from '@backstage/backend-plugin-api'; +import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { NotAllowedError, NotFoundError } from '@backstage/errors'; +import { CatalogService } from '@backstage/plugin-catalog-node'; + +export const createWhoAmIAction = ({ + auth, + catalog, + userInfo, + actionsRegistry, +}: { + auth: AuthService; + catalog: CatalogService; + userInfo: UserInfoService; + actionsRegistry: ActionsRegistryService; +}) => { + actionsRegistry.register({ + name: 'who-am-i', + title: 'Who Am I', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: + 'Returns the catalog entity and user info for the currently authenticated user. This action requires user credentials and cannot be used with service or unauthenticated credentials.', + schema: { + input: z => z.object({}), + output: z => + z.object({ + entity: z + .object({}) + .passthrough() + .describe('The full catalog entity for the authenticated user'), + userInfo: z + .object({ + userEntityRef: z + .string() + .describe( + 'The entity ref of the user, e.g. user:default/jane.doe', + ), + ownershipEntityRefs: z + .array(z.string()) + .describe('Entity refs that the user claims ownership through'), + }) + .describe( + 'User identity information extracted from the authentication token', + ), + }), + }, + action: async ({ credentials }) => { + if (!auth.isPrincipal(credentials, 'user')) { + throw new NotAllowedError('This action requires user credentials'); + } + + const { userEntityRef } = credentials.principal; + + const [entity, info] = await Promise.all([ + catalog.getEntityByRef(userEntityRef, { credentials }), + userInfo.getUserInfo(credentials), + ]); + + if (!entity) { + throw new NotFoundError( + `User entity not found in the catalog for "${userEntityRef}"`, + ); + } + + return { + output: { + entity, + userInfo: info, + }, + }; + }, + }); +}; diff --git a/packages/frontend-defaults/src/createPublicSignInApp.tsx b/plugins/auth-backend/src/actions/index.ts similarity index 55% rename from packages/frontend-defaults/src/createPublicSignInApp.tsx rename to plugins/auth-backend/src/actions/index.ts index 0015d9403f..28c611cca0 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.tsx +++ b/plugins/auth-backend/src/actions/index.ts @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { AuthService, UserInfoService } from '@backstage/backend-plugin-api'; +import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { createWhoAmIAction } from './createWhoAmIAction'; -import { appModulePublicSignIn } from '@backstage/plugin-app/alpha'; -import { CreateAppOptions, createApp } from './createApp'; - -/** - * @public - * @deprecated Use {@link @backstage/plugin-app/alpha#appModulePublicSignIn} instead. - */ -export function createPublicSignInApp(options?: CreateAppOptions) { - return createApp({ - ...options, - features: [...(options?.features ?? []), appModulePublicSignIn], - }); -} +export const createAuthActions = (options: { + auth: AuthService; + actionsRegistry: ActionsRegistryService; + catalog: CatalogService; + userInfo: UserInfoService; +}) => { + createWhoAmIAction(options); +}; diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts index df42ddce9e..3e721d18a7 100644 --- a/plugins/auth-backend/src/authPlugin.ts +++ b/plugins/auth-backend/src/authPlugin.ts @@ -24,7 +24,9 @@ import { AuthProviderFactory, authProvidersExtensionPoint, } from '@backstage/plugin-auth-node'; +import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { createAuthActions } from './actions'; import { createRouter } from './service/router'; import { OfflineAccessService } from './service/OfflineAccessService'; @@ -70,6 +72,8 @@ export const authPlugin = createBackendPlugin({ httpAuth: coreServices.httpAuth, lifecycle: coreServices.lifecycle, catalog: catalogServiceRef, + actionsRegistry: actionsRegistryServiceRef, + userInfo: coreServices.userInfo, }, async init({ httpRouter, @@ -81,6 +85,8 @@ export const authPlugin = createBackendPlugin({ httpAuth, lifecycle, catalog, + actionsRegistry, + userInfo, }) { const refreshTokensEnabled = config.getOptionalBoolean( 'auth.experimentalRefreshToken.enabled', @@ -112,6 +118,8 @@ export const authPlugin = createBackendPlugin({ allow: 'unauthenticated', }); httpRouter.use(router); + + createAuthActions({ auth, catalog, userInfo, actionsRegistry }); }, }); }, diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts index e2ab2390b6..5dec51ed85 100644 --- a/plugins/auth-backend/src/migrations.test.ts +++ b/plugins/auth-backend/src/migrations.test.ts @@ -385,4 +385,67 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20251217120000_drop_oidc_clients_fk.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20251217120000_drop_oidc_clients_fk.js'); + + // Create a client for DCR sessions + await knex + .insert({ + client_id: 'dcr-client-id', + client_secret: 'test-client-secret', + client_name: 'DCR Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + }) + .into('oidc_clients'); + + // Create a DCR session (has matching client in oidc_clients) + await knex + .insert({ + id: 'dcr-session', + client_id: 'dcr-client-id', + redirect_uri: 'https://example.com/callback', + response_type: 'code', + status: 'pending', + expires_at: new Date(Date.now() + 3600000), + }) + .into('oauth_authorization_sessions'); + + // Apply migration - drops FK constraint + await migrateUpOnce(knex); + + // Now we can insert a CIMD session (URL-based client_id not in oidc_clients) + await knex + .insert({ + id: 'cimd-session', + client_id: 'https://example.com/.well-known/oauth-client/cli', + redirect_uri: 'http://localhost:8080/callback', + response_type: 'code', + status: 'pending', + expires_at: new Date(Date.now() + 3600000), + }) + .into('oauth_authorization_sessions'); + + // Verify both sessions exist + await expect( + knex('oauth_authorization_sessions').select('id').orderBy('id'), + ).resolves.toEqual([{ id: 'cimd-session' }, { id: 'dcr-session' }]); + + // Rollback - should delete CIMD sessions and re-add FK + await migrateDownOnce(knex); + + // CIMD session should be deleted, DCR session should remain + await expect( + knex('oauth_authorization_sessions').select('id'), + ).resolves.toEqual([{ id: 'dcr-session' }]); + + await knex.destroy(); + }, + ); }); diff --git a/plugins/auth-backend/src/service/CimdClient.test.ts b/plugins/auth-backend/src/service/CimdClient.test.ts new file mode 100644 index 0000000000..fe7100b400 --- /dev/null +++ b/plugins/auth-backend/src/service/CimdClient.test.ts @@ -0,0 +1,581 @@ +/* + * 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 { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { isCimdUrl, validateCimdUrl, fetchCimdMetadata } from './CimdClient'; +import * as dns from 'node:dns/promises'; + +jest.mock('dns/promises'); +const mockDnsLookup = dns.lookup as jest.MockedFunction; + +const server = setupServer(); +registerMswTestHooks(server); + +describe('CimdClient', () => { + const originalNodeEnv = process.env.NODE_ENV; + + beforeEach(() => { + jest.resetAllMocks(); + // Default to public IP for DNS lookups + mockDnsLookup.mockResolvedValue([ + { address: '93.184.216.34', family: 4 }, + ] as any); + // Set development mode for tests that need localhost HTTP + (process.env as Record).NODE_ENV = + 'development'; + }); + + afterEach(() => { + (process.env as Record).NODE_ENV = + originalNodeEnv; + }); + + describe('isCimdUrl', () => { + it('should return true for valid CIMD URLs', () => { + expect(isCimdUrl('https://example.com/oauth-metadata.json')).toBe(true); + expect(isCimdUrl('https://example.com/path/to/metadata')).toBe(true); + expect( + isCimdUrl('https://sub.example.com/.well-known/oauth-client'), + ).toBe(true); + }); + + it('should return false for URLs without path', () => { + expect(isCimdUrl('https://example.com')).toBe(false); + expect(isCimdUrl('https://example.com/')).toBe(false); + }); + + it('should return false for non-HTTPS URLs on public hosts', () => { + expect(isCimdUrl('http://example.com/metadata')).toBe(false); + }); + + it('should return true for HTTP localhost URLs (development)', () => { + expect( + isCimdUrl( + 'http://localhost:7007/api/auth/.well-known/oauth-client/cli', + ), + ).toBe(true); + expect( + isCimdUrl( + 'http://127.0.0.1:7007/api/auth/.well-known/oauth-client/cli', + ), + ).toBe(true); + expect(isCimdUrl('http://localhost/path')).toBe(true); + }); + + it('should return false for HTTP localhost URLs in production', () => { + (process.env as Record).NODE_ENV = + 'production'; + expect(isCimdUrl('http://localhost:7007/path')).toBe(false); + expect(isCimdUrl('http://127.0.0.1:7007/path')).toBe(false); + }); + + it('should return false for non-URL strings', () => { + expect(isCimdUrl('not-a-url')).toBe(false); + expect(isCimdUrl('uuid-like-client-id')).toBe(false); + expect(isCimdUrl('')).toBe(false); + }); + + it('should return false for URLs with query strings', () => { + expect(isCimdUrl('https://example.com/metadata?foo=bar')).toBe(false); + }); + + it('should return false for URLs with dot path segments', () => { + expect(isCimdUrl('https://example.com/./metadata')).toBe(false); + expect(isCimdUrl('https://example.com/../metadata')).toBe(false); + }); + + it('should return false for URLs with fragments', () => { + expect(isCimdUrl('https://example.com/metadata#section')).toBe(false); + }); + }); + + describe('validateCimdUrl', () => { + it('should return URL for valid CIMD URLs', () => { + const url = validateCimdUrl('https://example.com/metadata.json'); + expect(url.href).toBe('https://example.com/metadata.json'); + }); + + it('should throw for non-HTTPS URLs on public hosts', () => { + expect(() => validateCimdUrl('http://example.com/metadata')).toThrow( + 'must use HTTPS', + ); + }); + + it('should allow HTTP for localhost URLs (development)', () => { + const url1 = validateCimdUrl( + 'http://localhost:7007/api/auth/.well-known/oauth-client/cli', + ); + expect(url1.href).toBe( + 'http://localhost:7007/api/auth/.well-known/oauth-client/cli', + ); + + const url2 = validateCimdUrl( + 'http://127.0.0.1:7007/api/auth/.well-known/oauth-client/cli', + ); + expect(url2.href).toBe( + 'http://127.0.0.1:7007/api/auth/.well-known/oauth-client/cli', + ); + }); + + it('should throw for HTTP localhost URLs in production', () => { + (process.env as Record).NODE_ENV = + 'production'; + expect(() => validateCimdUrl('http://localhost:7007/path')).toThrow( + 'must use HTTPS', + ); + expect(() => validateCimdUrl('http://127.0.0.1:7007/path')).toThrow( + 'must use HTTPS', + ); + }); + + it('should throw for URLs without path', () => { + expect(() => validateCimdUrl('https://example.com')).toThrow( + 'must have a path component', + ); + expect(() => validateCimdUrl('https://example.com/')).toThrow( + 'must have a path component', + ); + }); + + it('should throw for URLs with fragments', () => { + expect(() => + validateCimdUrl('https://example.com/metadata#fragment'), + ).toThrow('must not contain a fragment'); + }); + + it('should throw for URLs with credentials', () => { + expect(() => + validateCimdUrl('https://user:pass@example.com/metadata'), + ).toThrow('must not contain credentials'); + }); + + it('should throw for URLs with query strings', () => { + expect(() => + validateCimdUrl('https://example.com/metadata?foo=bar'), + ).toThrow('must not contain a query string'); + }); + + it('should throw for URLs with dot path segments', () => { + expect(() => validateCimdUrl('https://example.com/./metadata')).toThrow( + 'must not contain dot segments', + ); + expect(() => validateCimdUrl('https://example.com/../metadata')).toThrow( + 'must not contain dot segments', + ); + expect(() => + validateCimdUrl('https://example.com/path/../other'), + ).toThrow('must not contain dot segments'); + }); + + it('should throw for invalid URLs', () => { + expect(() => validateCimdUrl('not-a-url')).toThrow('not a valid URL'); + }); + }); + + describe('fetchCimdMetadata', () => { + const validMetadata = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + }; + + it('should fetch and return valid metadata', async () => { + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(validMetadata)); + }, + ), + ); + + const result = await fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }); + + expect(result).toEqual({ + clientId: 'https://example.com/oauth-metadata.json', + clientName: 'Test Client', + redirectUris: ['http://localhost:8080/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: undefined, + }); + }); + + it('should use client_id as client_name if not provided', async () => { + const metadataWithoutName = { + client_id: 'https://example.com/oauth-metadata.json', + redirect_uris: ['http://localhost:8080/callback'], + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(metadataWithoutName)); + }, + ), + ); + + const result = await fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }); + + expect(result.clientName).toBe('https://example.com/oauth-metadata.json'); + }); + + describe('SSRF protection', () => { + it('should throw for private IP addresses (192.168.x.x)', async () => { + mockDnsLookup.mockResolvedValue([ + { address: '192.168.1.1', family: 4 }, + ] as any); + + await expect( + fetchCimdMetadata({ + clientId: 'https://internal.example.com/metadata', + }), + ).rejects.toThrow('Invalid client_id URL'); + }); + + it('should throw for loopback addresses (127.x.x.x)', async () => { + mockDnsLookup.mockResolvedValue([ + { address: '127.0.0.1', family: 4 }, + ] as any); + + await expect( + fetchCimdMetadata({ + clientId: 'https://localhost.example.com/metadata', + }), + ).rejects.toThrow('Invalid client_id URL'); + }); + + it('should throw for 10.x.x.x addresses', async () => { + mockDnsLookup.mockResolvedValue([ + { address: '10.0.0.1', family: 4 }, + ] as any); + + await expect( + fetchCimdMetadata({ + clientId: 'https://internal.example.com/metadata', + }), + ).rejects.toThrow('Invalid client_id URL'); + }); + + it('should throw for 172.16-31.x.x addresses', async () => { + mockDnsLookup.mockResolvedValue([ + { address: '172.16.0.1', family: 4 }, + ] as any); + + await expect( + fetchCimdMetadata({ + clientId: 'https://internal.example.com/metadata', + }), + ).rejects.toThrow('Invalid client_id URL'); + }); + + it('should throw for IPv6 loopback', async () => { + mockDnsLookup.mockResolvedValue([{ address: '::1', family: 6 }] as any); + + await expect( + fetchCimdMetadata({ + clientId: 'https://internal.example.com/metadata', + }), + ).rejects.toThrow('Invalid client_id URL'); + }); + }); + + describe('redirect protection', () => { + it('should reject redirects to prevent SSRF via redirect bypass', async () => { + const redirectTarget = jest.fn(); + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res( + ctx.status(302), + ctx.set('Location', 'http://127.0.0.1:8080/internal'), + ); + }, + ), + rest.get('http://127.0.0.1:8080/internal', (_req, res, ctx) => { + redirectTarget(); + return res( + ctx.json({ + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Sneaky Client', + redirect_uris: ['http://localhost:8080/callback'], + }), + ); + }), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Failed to fetch client metadata'); + + expect(redirectTarget).not.toHaveBeenCalled(); + }); + }); + + describe('HTTP error handling', () => { + it('should throw for network errors', async () => { + server.use( + rest.get('https://example.com/oauth-metadata.json', (_req, res) => { + return res.networkError('Connection refused'); + }), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Failed to fetch client metadata'); + }); + + it('should throw for non-OK response', async () => { + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Failed to fetch client metadata'); + }); + }); + + describe('metadata validation', () => { + it('should throw for invalid JSON', async () => { + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.body('not json')); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Invalid client metadata document'); + }); + + it('should throw for client_id mismatch', async () => { + const mismatchedMetadata = { + client_id: 'https://different.com/metadata', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(mismatchedMetadata)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Client ID mismatch in metadata document'); + }); + + it('should throw for missing redirect_uris', async () => { + const noRedirectUris = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(noRedirectUris)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('at least one redirect_uri'); + }); + + it('should throw for empty redirect_uris', async () => { + const emptyRedirectUris = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: [], + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(emptyRedirectUris)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('at least one redirect_uri'); + }); + }); + + describe('security constraints', () => { + it('should throw for metadata containing client_secret', async () => { + const withSecret = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + client_secret: 'should-not-be-here', + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(withSecret)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Client metadata must not contain client_secret'); + }); + + it('should throw for metadata containing client_secret_expires_at', async () => { + const withSecretExpiry = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + client_secret_expires_at: 12345, + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(withSecretExpiry)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('Client metadata must not contain client_secret'); + }); + + it('should throw for forbidden token_endpoint_auth_method', async () => { + const withForbiddenAuth = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + token_endpoint_auth_method: 'client_secret_basic', + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(withForbiddenAuth)); + }, + ), + ); + + await expect( + fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }), + ).rejects.toThrow('forbidden auth method'); + }); + + it('should allow token_endpoint_auth_method: none', async () => { + const withNoneAuth = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + token_endpoint_auth_method: 'none', + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(withNoneAuth)); + }, + ), + ); + + const result = await fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }); + + expect(result.clientId).toBe('https://example.com/oauth-metadata.json'); + }); + + it('should allow token_endpoint_auth_method: private_key_jwt', async () => { + const withPrivateKeyAuth = { + client_id: 'https://example.com/oauth-metadata.json', + client_name: 'Test Client', + redirect_uris: ['http://localhost:8080/callback'], + token_endpoint_auth_method: 'private_key_jwt', + }; + + server.use( + rest.get( + 'https://example.com/oauth-metadata.json', + (_req, res, ctx) => { + return res(ctx.json(withPrivateKeyAuth)); + }, + ), + ); + + const result = await fetchCimdMetadata({ + clientId: 'https://example.com/oauth-metadata.json', + }); + + expect(result.clientId).toBe('https://example.com/oauth-metadata.json'); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/service/CimdClient.ts b/plugins/auth-backend/src/service/CimdClient.ts new file mode 100644 index 0000000000..9520517f25 --- /dev/null +++ b/plugins/auth-backend/src/service/CimdClient.ts @@ -0,0 +1,259 @@ +/* + * 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 { InputError, isError } from '@backstage/errors'; +import { lookup } from 'node:dns/promises'; +import ipaddr from 'ipaddr.js'; + +const FETCH_TIMEOUT_MS = 10000; +const MAX_RESPONSE_BYTES = 64 * 1024; + +/** Auth methods that require a client secret - forbidden for CIMD clients */ +const FORBIDDEN_AUTH_METHODS = [ + 'client_secret_basic', + 'client_secret_post', + 'client_secret_jwt', +]; + +/** + * Raw metadata document from a CIMD URL. + * Note: client_secret fields are included for validation (must NOT be present). + */ +interface CimdMetadata { + client_id: string; + client_name?: string; + redirect_uris: string[]; + response_types?: string[]; + grant_types?: string[]; + scope?: string; + token_endpoint_auth_method?: string; + client_secret?: string; + client_secret_expires_at?: number; +} + +/** Validated CIMD client info */ +export interface CimdClientInfo { + clientId: string; + clientName: string; + redirectUris: string[]; + responseTypes: string[]; + grantTypes: string[]; + scope?: string; +} + +/** + * Validates and parses a CIMD URL per the IETF draft specification. + * Requires HTTPS for production, but allows HTTP for localhost (development). + * + * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ + * @throws InputError if the URL is invalid per the CIMD spec + */ +export function validateCimdUrl(clientId: string): URL { + // Per IETF draft: MUST NOT contain single-dot or double-dot path segments + // Check before URL parsing since the URL constructor normalizes these away + if (/\/\.\.?(\/|$)/.test(clientId)) { + throw new InputError( + 'Invalid client_id: path must not contain dot segments', + ); + } + + let url: URL; + try { + url = new URL(clientId); + } catch { + throw new InputError('Invalid client_id: not a valid URL'); + } + + const isHttps = url.protocol === 'https:'; + const isLocalHttp = + url.protocol === 'http:' && + (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && + process.env.NODE_ENV === 'development'; + + if (!isHttps && !isLocalHttp) { + throw new InputError( + 'Invalid client_id: must use HTTPS (or HTTP for localhost in development)', + ); + } + + if (url.pathname === '' || url.pathname === '/') { + throw new InputError('Invalid client_id: must have a path component'); + } + + if (url.hash) { + throw new InputError('Invalid client_id: must not contain a fragment'); + } + + if (url.username || url.password) { + throw new InputError('Invalid client_id: must not contain credentials'); + } + + // Per IETF draft: SHOULD NOT include a query string + // We reject this for stricter compliance and security + if (url.search) { + throw new InputError('Invalid client_id: must not contain a query string'); + } + + return url; +} + +/** + * Checks if a client_id is a valid CIMD URL. + * Requires HTTPS for production, but allows HTTP for localhost (development). + */ +export function isCimdUrl(clientId: string): boolean { + try { + validateCimdUrl(clientId); + return true; + } catch { + return false; + } +} + +/** + * SSRF (Server-Side Request Forgery) Protection + * + * When fetching CIMD metadata from client-provided URLs, we must prevent + * attackers from tricking Backstage into accessing internal resources. + * For example, an attacker could provide a URL that resolves to: + * - 127.0.0.1 (localhost services) + * - 10.x.x.x, 172.16-31.x.x, 192.168.x.x (internal network) + * - Cloud metadata endpoints (169.254.169.254) + * + * We use ipaddr.js to check if resolved IPs are in non-public ranges. + * Only 'unicast' (public internet) addresses are allowed. + * + * @see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/ + * Section 5.1 - Security Considerations + */ +function isNonPublicIp(ip: string): boolean { + try { + const addr = ipaddr.parse(ip); + const range = addr.range(); + // Only allow public unicast addresses + return range !== 'unicast'; + } catch { + // If we can't parse the IP, treat it as non-public and block it + return true; + } +} + +async function validateHostNotPrivate(hostname: string): Promise { + try { + const addresses = await lookup(hostname, { all: true }); + const nonPublicAddr = addresses.find(addr => isNonPublicIp(addr.address)); + if (nonPublicAddr) { + throw new InputError('Invalid client_id URL'); + } + } catch (error) { + if (isError(error) && error.name === 'InputError') throw error; + throw new InputError('Failed to fetch client metadata'); + } +} + +function validateMetadata( + metadata: CimdMetadata, + expectedClientId: string, +): void { + if (metadata.client_id !== expectedClientId) { + throw new InputError('Client ID mismatch in metadata document'); + } + + if ( + !Array.isArray(metadata.redirect_uris) || + metadata.redirect_uris.length === 0 + ) { + throw new InputError('Metadata must include at least one redirect_uri'); + } + + for (const uri of metadata.redirect_uris) { + if (!URL.canParse(uri)) { + throw new InputError(`Invalid redirect_uri in metadata: ${uri}`); + } + } + + if ( + metadata.client_secret !== undefined || + metadata.client_secret_expires_at !== undefined + ) { + throw new InputError('Client metadata must not contain client_secret'); + } + + if ( + metadata.token_endpoint_auth_method && + FORBIDDEN_AUTH_METHODS.includes(metadata.token_endpoint_auth_method) + ) { + throw new InputError('Client metadata uses forbidden auth method'); + } +} + +/** + * Fetches and validates a CIMD metadata document. + * @throws InputError if fetching or validation fails + */ +export async function fetchCimdMetadata(opts: { + clientId: string; + validatedUrl?: URL; +}): Promise { + const url = opts.validatedUrl ?? validateCimdUrl(opts.clientId); + + // Skip SSRF validation for localhost in development only + const isLocalhostDev = + (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && + process.env.NODE_ENV === 'development'; + if (!isLocalhostDev) { + await validateHostNotPrivate(url.hostname); + } + + let response: Response; + try { + response = await fetch(url.toString(), { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + redirect: 'error', + }); + } catch { + throw new InputError('Failed to fetch client metadata'); + } + + if (!response.ok) { + throw new InputError('Failed to fetch client metadata'); + } + + const contentLength = Number(response.headers.get('content-length')); + if (contentLength > MAX_RESPONSE_BYTES) { + throw new InputError('Client metadata document too large'); + } + + let metadata: CimdMetadata; + try { + metadata = await response.json(); + } catch { + throw new InputError('Invalid client metadata document'); + } + + validateMetadata(metadata, opts.clientId); + + return { + clientId: metadata.client_id, + clientName: metadata.client_name || metadata.client_id, + redirectUris: metadata.redirect_uris, + responseTypes: metadata.response_types || ['code'], + grantTypes: metadata.grant_types || ['authorization_code'], + scope: metadata.scope, + }; +} diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 0f86b8f413..6027f27406 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -35,6 +35,22 @@ import { AuthDatabase } from '../database/AuthDatabase'; import { OidcService } from '../service/OidcService'; import { TokenIssuer } from '../identity/types'; import { OfflineAccessService } from './OfflineAccessService'; +import { CimdClientInfo, isCimdUrl } from './CimdClient'; + +jest.mock('./CimdClient', () => { + const actual = jest.requireActual('./CimdClient'); + return { + ...actual, + fetchCimdMetadata: jest.fn(), + }; +}); + +import * as CimdClient from './CimdClient'; + +const mockFetchCimdMetadata = + CimdClient.fetchCimdMetadata as jest.MockedFunction< + typeof CimdClient.fetchCimdMetadata + >; jest.setTimeout(60_000); @@ -43,6 +59,10 @@ describe('OidcRouter', () => { const MOCK_USER_ENTITY_REF = 'user:default/test-user'; const databases = TestDatabases.create(); + afterEach(() => { + mockFetchCimdMetadata.mockReset(); + }); + async function createRouter(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); @@ -89,6 +109,7 @@ describe('OidcRouter', () => { userInfo: userInfoDatabase, oidc: oidcDatabase, config: mockConfig, + logger: mockServices.logger.mock(), }); const oidcRouter = OidcRouter.create({ @@ -174,6 +195,7 @@ describe('OidcRouter', () => { userInfo: userInfoDatabase, oidc: oidcDatabase, config: mockConfig, + logger: mockServices.logger.mock(), offlineAccess, }); @@ -401,9 +423,9 @@ describe('OidcRouter', () => { }) .expect(302); - expect(response.header.location).toMatch( - /^http:\/\/localhost:3000\/oauth2\/authorize\/[a-f0-9-]+$/, - ); + const location = new URL(response.header.location); + expect(location.origin).toBe('http://localhost:3000'); + expect(location.pathname).toMatch(/^\/oauth2\/authorize\/[a-f0-9-]+$/); }); it('should get auth session details', async () => { @@ -513,11 +535,11 @@ describe('OidcRouter', () => { .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); - expect(response.body).toEqual({ - redirectUrl: expect.stringMatching( - /^https:\/\/example\.com\/callback\?code=[\w-]+&state=test-state$/, - ), - }); + const redirectUrl = new URL(response.body.redirectUrl); + expect(redirectUrl.origin).toBe('https://example.com'); + expect(redirectUrl.pathname).toBe('/callback'); + expect(redirectUrl.searchParams.get('code')).toBeDefined(); + expect(redirectUrl.searchParams.get('state')).toBe('test-state'); }); it('should reject auth session', async () => { @@ -572,11 +594,14 @@ describe('OidcRouter', () => { .post(`/api/auth/v1/sessions/${authSession.id}/reject`) .expect(200); - expect(response.body).toEqual({ - redirectUrl: expect.stringMatching( - /^https:\/\/example\.com\/callback\?error=access_denied&error_description=User\+denied\+the\+request&state=test-state$/, - ), - }); + const redirectUrl = new URL(response.body.redirectUrl); + expect(redirectUrl.origin).toBe('https://example.com'); + expect(redirectUrl.pathname).toBe('/callback'); + expect(redirectUrl.searchParams.get('error')).toBe('access_denied'); + expect(redirectUrl.searchParams.get('error_description')).toBe( + 'User denied the request', + ); + expect(redirectUrl.searchParams.get('state')).toBe('test-state'); }); }); @@ -1096,5 +1121,252 @@ describe('OidcRouter', () => { .expect(400); }); }); + + describe('CIMD metadata endpoint', () => { + it('should return 404 when CIMD is not enabled', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get('/api/auth/.well-known/oauth-client/cli.json') + .expect(404); + + expect(response.body).toEqual({ + error: 'not_found', + error_description: 'Client ID metadata documents not enabled', + }); + }); + + it('should return CIMD document when enabled', async () => { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + const authDatabase = AuthDatabase.create({ + getClient: async () => knex, + }); + + const oidcDatabase = await OidcDatabase.create({ + database: authDatabase, + }); + + const userInfoDatabase = await UserInfoDatabase.create({ + database: authDatabase, + }); + + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as unknown as jest.Mocked; + + const oidcRouter = OidcRouter.create({ + auth: mockServices.auth.mock(), + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7007/api/auth', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), + userInfo: userInfoDatabase, + oidc: oidcDatabase, + httpAuth: mockServices.httpAuth.mock(), + config: mockServices.rootConfig({ + data: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + }, + }, + }, + }), + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(oidcRouter.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get('/api/auth/.well-known/oauth-client/cli.json') + .expect(200); + + expect(response.body).toEqual({ + client_id: + 'http://localhost:7007/api/auth/.well-known/oauth-client/cli.json', + client_name: 'Backstage CLI', + redirect_uris: ['http://127.0.0.1:8055/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + token_endpoint_auth_method: 'none', + scope: 'openid offline_access', + }); + }); + }); + + describe('CIMD authorization', () => { + it('should enable authorization routes when only CIMD is enabled (not DCR)', async () => { + const cimdClientId = 'https://example.com/oauth-client'; + const cimdMetadata: CimdClientInfo = { + clientId: cimdClientId, + clientName: 'Test CLI Client', + redirectUris: ['http://localhost:8080/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }; + mockFetchCimdMetadata.mockResolvedValue(cimdMetadata); + + // Verify isCimdUrl works correctly + expect(isCimdUrl(cimdClientId)).toBe(true); + + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + const authDatabase = AuthDatabase.create({ + getClient: async () => knex, + }); + + const oidcDatabase = await OidcDatabase.create({ + database: authDatabase, + }); + + const userInfoDatabase = await UserInfoDatabase.create({ + database: authDatabase, + }); + + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as unknown as jest.Mocked; + + const mockAuth = mockServices.auth.mock(); + const mockHttpAuth = mockServices.httpAuth.mock(); + // Only CIMD enabled, NOT DCR + const mockConfig = mockServices.rootConfig({ + data: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + }, + // DCR is NOT enabled + }, + }, + }); + + const oidcRouter = OidcRouter.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7007/api/auth', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), + userInfo: userInfoDatabase, + oidc: oidcDatabase, + httpAuth: mockHttpAuth, + config: mockConfig, + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(oidcRouter.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const codeVerifier = 'test-code-verifier-for-pkce'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + // /v1/authorize should work with CIMD-only config + const authorizeResponse = await request(server) + .get('/api/auth/v1/authorize') + .query({ + client_id: cimdClientId, + redirect_uri: 'http://localhost:8080/callback', + response_type: 'code', + scope: 'openid', + state: 'test-state', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }); + + expect(authorizeResponse.status).toBe(302); + + // Should redirect to consent screen + expect(mockFetchCimdMetadata).toHaveBeenCalledWith({ + clientId: cimdClientId, + validatedUrl: expect.any(URL), + }); + const location = new URL(authorizeResponse.header.location); + expect(location.origin).toBe('http://localhost:3000'); + expect(location.pathname).toMatch(/^\/oauth2\/authorize\/[a-f0-9-]+$/); + + // /v1/register should NOT be available (DCR disabled) + await request(server) + .post('/api/auth/v1/register') + .send({ + client_name: 'Test Client', + redirect_uris: ['https://example.com/callback'], + }) + .expect(404); + }); + }); }); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 00861e09e4..dc921a9489 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -27,11 +27,14 @@ import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; import { OfflineAccessService } from './OfflineAccessService'; import { json } from 'express'; -import { readDcrTokenExpiration } from './readTokenExpiration'; import { z } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { OidcError } from './OidcError'; +function ensureTrailingSlash(url: string): string { + return url.endsWith('/') ? url : `${url}/`; +} + const authorizeQuerySchema = z.object({ client_id: z.string().min(1), redirect_uri: z.string().url(), @@ -81,12 +84,13 @@ function validateRequest(schema: z.ZodSchema, data: unknown): T { return parseResult.data; } -async function authenticateClient( - req: { headers: { authorization?: string } }, - oidc: OidcService, - bodyClientId?: string, - bodyClientSecret?: string, -): Promise<{ clientId: string; clientSecret: string }> { +async function authenticateClient(opts: { + req: { headers: { authorization?: string } }; + oidc: OidcService; + bodyClientId?: string; + bodyClientSecret?: string; +}): Promise<{ clientId: string; clientSecret: string }> { + const { req, oidc, bodyClientId, bodyClientSecret } = opts; let clientId: string | undefined; let clientSecret: string | undefined; @@ -141,6 +145,7 @@ export class OidcRouter { private readonly appUrl: string; private readonly httpAuth: HttpAuthService; private readonly config: RootConfigService; + private readonly baseUrl: string; private constructor( oidc: OidcService, @@ -149,6 +154,7 @@ export class OidcRouter { appUrl: string, httpAuth: HttpAuthService, config: RootConfigService, + baseUrl: string, ) { this.oidc = oidc; this.logger = logger; @@ -156,6 +162,7 @@ export class OidcRouter { this.appUrl = appUrl; this.httpAuth = httpAuth; this.config = config; + this.baseUrl = baseUrl; } static create(options: { @@ -177,6 +184,7 @@ export class OidcRouter { options.appUrl, options.httpAuth, options.config, + options.baseUrl, ); } @@ -200,6 +208,32 @@ export class OidcRouter { res.json({ keys }); }); + // CIMD metadata endpoint for the Backstage CLI + // Automatically available when CIMD is enabled + router.get('/.well-known/oauth-client/cli.json', (_req, res) => { + const cimdEnabled = this.config.getOptionalBoolean( + 'auth.experimentalClientIdMetadataDocuments.enabled', + ); + + if (!cimdEnabled) { + res.status(404).json({ + error: 'not_found', + error_description: 'Client ID metadata documents not enabled', + }); + return; + } + + res.json({ + client_id: `${this.baseUrl}/.well-known/oauth-client/cli.json`, + client_name: 'Backstage CLI', + redirect_uris: ['http://127.0.0.1:8055/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + token_endpoint_auth_method: 'none', + scope: 'openid offline_access', + }); + }); + // UserInfo endpoint // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo // Returns claims about the authenticated user using an access token @@ -223,8 +257,11 @@ export class OidcRouter { const dcrEnabled = this.config.getOptionalBoolean( 'auth.experimentalDynamicClientRegistration.enabled', ); + const cimdEnabled = this.config.getOptionalBoolean( + 'auth.experimentalClientIdMetadataDocuments.enabled', + ); - if (dcrEnabled) { + if (dcrEnabled || cimdEnabled) { // Authorization endpoint // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Handles the initial authorization request from the client, validates parameters, @@ -389,8 +426,6 @@ export class OidcRouter { client_secret: bodyClientSecret, } = validateRequest(tokenRequestBodySchema, req.body); - const expiresIn = readDcrTokenExpiration(this.config); - try { // Handle authorization_code grant type if (grantType === 'authorization_code') { @@ -407,7 +442,6 @@ export class OidcRouter { redirectUri, codeVerifier, grantType, - expiresIn, }); return res.json({ @@ -439,12 +473,12 @@ export class OidcRouter { let authenticatedClientId: string | undefined; if (hasCredentials) { - const { clientId: authedId } = await authenticateClient( + const { clientId: authedId } = await authenticateClient({ req, - this.oidc, + oidc: this.oidc, bodyClientId, bodyClientSecret, - ); + }); authenticatedClientId = authedId; } @@ -520,12 +554,12 @@ export class OidcRouter { client_secret: bodyClientSecret, } = validateRequest(revokeRequestBodySchema, req.body ?? {}); - await authenticateClient( + await authenticateClient({ req, - this.oidc, + oidc: this.oidc, bodyClientId, bodyClientSecret, - ); + }); try { await this.oidc.revokeRefreshToken(token); @@ -546,9 +580,3 @@ export class OidcRouter { return router; } } -function ensureTrailingSlash(appUrl: string): string { - if (appUrl.endsWith('/')) { - return appUrl; - } - return `${appUrl}/`; -} diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 6242795565..37eed97092 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -18,7 +18,9 @@ import { TestDatabaseId, TestDatabases, } from '@backstage/backend-test-utils'; +import { JsonObject } from '@backstage/types'; import { OidcService } from './OidcService'; +import { OfflineAccessService } from './OfflineAccessService'; import { BackstageCredentials, BackstageServicePrincipal, @@ -30,13 +32,34 @@ import { OidcDatabase } from '../database/OidcDatabase'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import crypto from 'node:crypto'; import { AnyJWK, TokenIssuer } from '../identity/types'; +import { CimdClientInfo } from './CimdClient'; + +jest.mock('./CimdClient', () => ({ + ...jest.requireActual('./CimdClient'), + fetchCimdMetadata: jest.fn(), +})); + +import * as CimdClient from './CimdClient'; + +const mockFetchCimdMetadata = + CimdClient.fetchCimdMetadata as jest.MockedFunction< + typeof CimdClient.fetchCimdMetadata + >; jest.setTimeout(60_000); describe('OidcService', () => { const databases = TestDatabases.create(); - async function createOidcService(databaseId: TestDatabaseId) { + interface CreateOidcServiceOptions { + databaseId: TestDatabaseId; + config?: JsonObject; + offlineAccess?: OfflineAccessService; + } + + async function createOidcService(options: CreateOidcServiceOptions) { + const { databaseId, config: configData = {}, offlineAccess } = options; + const knex = await databases.init(databaseId); await knex.migrate.latest({ @@ -63,7 +86,7 @@ describe('OidcService', () => { getUserInfo: jest.fn(), } as unknown as jest.Mocked; - const mockConfig = mockServices.rootConfig.mock(); + const config = mockServices.rootConfig({ data: configData }); return { service: OidcService.create({ @@ -72,13 +95,14 @@ describe('OidcService', () => { baseUrl: 'http://mock-base-url', userInfo: mockUserInfo, oidc: oidcDatabase, - config: mockConfig, + config, + logger: mockServices.logger.mock(), + offlineAccess, }), mocks: { auth: mockAuth, tokenIssuer: mockTokenIssuer, userInfo: mockUserInfo, - config: mockConfig, }, }; } @@ -86,7 +110,7 @@ describe('OidcService', () => { describe.each(databases.eachSupportedId())('%p', databaseId => { describe('getConfiguration', () => { it('should return OIDC configuration', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const config = service.getConfiguration(); @@ -124,7 +148,7 @@ describe('OidcService', () => { describe('listPublicKeys', () => { it('should return public keys from token issuer', async () => { - const { service, mocks } = await createOidcService(databaseId); + const { service, mocks } = await createOidcService({ databaseId }); const mockKeys = [{ kid: 'key-1', use: 'sig' }] as AnyJWK[]; mocks.tokenIssuer.listPublicKeys.mockResolvedValue({ keys: mockKeys }); @@ -137,7 +161,7 @@ describe('OidcService', () => { describe('getUserInfo', () => { it('should return user info for valid token', async () => { - const { service, mocks } = await createOidcService(databaseId); + const { service, mocks } = await createOidcService({ databaseId }); const mockCredentials: BackstageCredentials = { principal: { type: 'user', @@ -172,7 +196,7 @@ describe('OidcService', () => { }); it('should throw error for non-user principal', async () => { - const { service, mocks } = await createOidcService(databaseId); + const { service, mocks } = await createOidcService({ databaseId }); const mockCredentials: BackstageCredentials = { principal: { @@ -196,7 +220,7 @@ describe('OidcService', () => { describe('registerClient', () => { it('should create a new client with generated credentials', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -220,14 +244,16 @@ describe('OidcService', () => { }); it('should throw an error for invalid redirect URI', async () => { - const { - service, - mocks: { config }, - } = await createOidcService(databaseId); - - config.getOptionalStringArray.mockReturnValue([ - 'https://example.com/*', - ]); + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalDynamicClientRegistration: { + allowedRedirectUriPatterns: ['https://example.com/*'], + }, + }, + }, + }); await expect( service.registerClient({ @@ -238,12 +264,16 @@ describe('OidcService', () => { }); it('should create a new client with valid redirect URI', async () => { - const { - service, - mocks: { config }, - } = await createOidcService(databaseId); - - config.getOptionalStringArray.mockReturnValue(['cursor:*']); + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalDynamicClientRegistration: { + allowedRedirectUriPatterns: ['cursor:*'], + }, + }, + }, + }); const client = await service.registerClient({ clientName: 'Test Client', @@ -257,8 +287,35 @@ describe('OidcService', () => { ); }); + it('should reject redirect URIs containing userinfo', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalDynamicClientRegistration: { + allowedRedirectUriPatterns: ['http://localhost:*'], + }, + }, + }, + }); + + await expect( + service.registerClient({ + clientName: 'Evil Client', + redirectUris: ['http://localhost:3000@attacker.example/callback'], + }), + ).rejects.toThrow('Invalid redirect_uri'); + + await expect( + service.registerClient({ + clientName: 'Evil Client', + redirectUris: ['http://user:pass@example.com/callback'], + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + it('should create a client with default values', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -277,7 +334,7 @@ describe('OidcService', () => { describe('createAuthorizationSession', () => { it('should create a authorization session for valid client', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -301,7 +358,7 @@ describe('OidcService', () => { }); it('should throw error for invalid client', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); await expect( service.createAuthorizationSession({ @@ -313,7 +370,7 @@ describe('OidcService', () => { }); it('should throw error for invalid redirect URI', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -330,7 +387,7 @@ describe('OidcService', () => { }); it('should throw error for unsupported response type', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -347,7 +404,7 @@ describe('OidcService', () => { }); it('should handle PKCE parameters', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -366,7 +423,7 @@ describe('OidcService', () => { }); it('should throw error for invalid PKCE method', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -387,7 +444,7 @@ describe('OidcService', () => { describe('approveAuthorizationSession', () => { it('should approve a valid authorization session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -412,7 +469,7 @@ describe('OidcService', () => { }); it('should throw error for invalid authorization session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); await expect( service.approveAuthorizationSession({ @@ -423,7 +480,7 @@ describe('OidcService', () => { }); it('should throw error when trying to approve an already approved session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -450,7 +507,7 @@ describe('OidcService', () => { }); it('should throw error when trying to approve an already rejected session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -479,7 +536,7 @@ describe('OidcService', () => { describe('getAuthorizationSession', () => { it('should return authorization session details', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -512,7 +569,7 @@ describe('OidcService', () => { }); it('should throw error when trying to get an already approved session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -538,7 +595,7 @@ describe('OidcService', () => { }); it('should throw error when trying to get an already rejected session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -566,7 +623,7 @@ describe('OidcService', () => { describe('rejectAuthorizationSession', () => { it('should reject a authorization session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -592,7 +649,7 @@ describe('OidcService', () => { }); it('should throw error for invalid authorization session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); await expect( service.rejectAuthorizationSession({ @@ -603,7 +660,7 @@ describe('OidcService', () => { }); it('should throw error when trying to reject an already approved session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -630,7 +687,7 @@ describe('OidcService', () => { }); it('should throw error when trying to reject an already rejected session', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -659,7 +716,7 @@ describe('OidcService', () => { describe('exchangeCodeForToken', () => { it('should exchange valid code for tokens', async () => { - const { service, mocks } = await createOidcService(databaseId); + const { service, mocks } = await createOidcService({ databaseId }); const mockToken = 'mock-jwt-token'; mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); @@ -686,7 +743,6 @@ describe('OidcService', () => { code, redirectUri: 'https://example.com/callback', grantType: 'authorization_code', - expiresIn: 3600, }); expect(tokenResult).toEqual({ @@ -698,61 +754,20 @@ describe('OidcService', () => { }); }); - it('should exchange valid code for tokens with custom expiration', async () => { - const { service, mocks } = await createOidcService(databaseId); - const mockToken = 'mock-jwt-token'; - mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); - - const client = await service.registerClient({ - clientName: 'Test Client', - redirectUris: ['https://example.com/callback'], - }); - - const authSession = await service.createAuthorizationSession({ - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - scope: 'openid', - }); - - const authResult = await service.approveAuthorizationSession({ - sessionId: authSession.id, - userEntityRef: 'user:default/test', - }); - - const code = new URL(authResult.redirectUrl).searchParams.get('code')!; - - const tokenResult = await service.exchangeCodeForToken({ - code, - redirectUri: 'https://example.com/callback', - grantType: 'authorization_code', - expiresIn: 6000, - }); - - expect(tokenResult).toEqual({ - accessToken: mockToken, - tokenType: 'Bearer', - expiresIn: 6000, - idToken: mockToken, - scope: 'openid', - }); - }); - it('should throw error for invalid grant type', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); await expect( service.exchangeCodeForToken({ code: 'test-code', redirectUri: 'https://example.com/callback', grantType: 'client_credentials', - expiresIn: 3600, }), ).rejects.toThrow('Unsupported grant type'); }); it('should handle PKCE verification', async () => { - const { service, mocks } = await createOidcService(databaseId); + const { service, mocks } = await createOidcService({ databaseId }); const mockToken = 'mock-jwt-token'; mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); @@ -787,14 +802,13 @@ describe('OidcService', () => { redirectUri: 'https://example.com/callback', grantType: 'authorization_code', codeVerifier, - expiresIn: 3600, }); expect(tokenResult.accessToken).toBe(mockToken); }); it('should throw error for invalid PKCE verifier', async () => { - const { service } = await createOidcService(databaseId); + const { service } = await createOidcService({ databaseId }); const client = await service.registerClient({ clientName: 'Test Client', @@ -823,10 +837,518 @@ describe('OidcService', () => { redirectUri: 'https://example.com/callback', grantType: 'authorization_code', codeVerifier: 'invalid-verifier', - expiresIn: 3600, }), ).rejects.toThrow('Invalid code verifier'); }); + + it('should still return access token when refresh token issuance fails', async () => { + const mockOfflineAccess = { + issueRefreshToken: jest + .fn() + .mockRejectedValue(new Error('DB constraint violation')), + } as unknown as OfflineAccessService; + + const { service, mocks } = await createOidcService({ + databaseId, + offlineAccess: mockOfflineAccess, + }); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid offline_access', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }); + + expect(tokenResult.accessToken).toBe(mockToken); + expect(tokenResult.refreshToken).toBeUndefined(); + expect(mockOfflineAccess.issueRefreshToken).toHaveBeenCalledTimes(1); + }); + }); + + describe('CIMD (Client ID Metadata Document) support', () => { + const cimdClientId = 'https://example.com/oauth-metadata.json'; + const cimdMetadata: CimdClientInfo = { + clientId: cimdClientId, + clientName: 'CIMD Test Client', + redirectUris: ['http://localhost:8080/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }; + + beforeEach(() => { + mockFetchCimdMetadata.mockResolvedValue(cimdMetadata); + }); + + afterEach(() => { + mockFetchCimdMetadata.mockReset(); + }); + + const pkceCodeVerifier = 'test-code-verifier-for-pkce'; + const pkceCodeChallenge = crypto + .createHash('sha256') + .update(pkceCodeVerifier) + .digest('base64url'); + const pkceParams = { + codeChallenge: pkceCodeChallenge, + codeChallengeMethod: 'S256' as const, + }; + + describe('getConfiguration', () => { + it('should include client_id_metadata_document_supported when CIMD is enabled', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + + const config = service.getConfiguration(); + + expect(config.client_id_metadata_document_supported).toBe(true); + }); + + it('should not include client_id_metadata_document_supported when CIMD is disabled', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: false }, + }, + }, + }); + + const config = service.getConfiguration(); + + expect(config).not.toHaveProperty( + 'client_id_metadata_document_supported', + ); + }); + }); + + describe('createAuthorizationSession with CIMD', () => { + it('should create authorization session for CIMD client', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + scope: 'openid', + ...pkceParams, + }); + + expect(authSession).toEqual({ + id: expect.any(String), + clientName: 'CIMD Test Client', + scope: 'openid', + redirectUri: 'http://localhost:8080/callback', + }); + expect(mockFetchCimdMetadata).toHaveBeenCalledWith({ + clientId: cimdClientId, + validatedUrl: expect.any(URL), + }); + }); + + it('should throw error when CIMD is disabled but URL client_id is provided', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: false }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + }), + ).rejects.toThrow('Client ID metadata documents not enabled'); + }); + + it('should throw error when client_id does not match allowedClientIdPatterns', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + allowedClientIdPatterns: ['https://trusted.com/*'], + }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, // https://example.com/oauth-metadata.json + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid client_id'); + }); + + it('should accept client_id matching allowedClientIdPatterns', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + allowedClientIdPatterns: ['https://example.com/*'], + }, + }, + }, + }); + + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + ...pkceParams, + }); + + expect(authSession).toEqual( + expect.objectContaining({ + id: expect.any(String), + clientName: 'CIMD Test Client', + }), + ); + }); + + it('should throw error for redirect_uri not in CIMD metadata', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://unauthorized.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Redirect URI not registered'); + }); + + it('should throw error when redirect_uri does not match allowedRedirectUriPatterns', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + allowedRedirectUriPatterns: ['https://*.example.com/*'], + }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should reject redirect_uri when CIMD metadata uses wildcard patterns', async () => { + mockFetchCimdMetadata.mockResolvedValue({ + ...cimdMetadata, + redirectUris: ['http://localhost:*/callback'], + }); + + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + allowedRedirectUriPatterns: ['http://localhost:*'], + }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + ...pkceParams, + }), + ).rejects.toThrow('Redirect URI not registered'); + }); + + it('should reject redirect_uri not exactly matching CIMD metadata', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + allowedRedirectUriPatterns: ['http://localhost:*'], + }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/other-path', + responseType: 'code', + ...pkceParams, + }), + ).rejects.toThrow('Redirect URI not registered'); + }); + + it('should require PKCE for CIMD clients', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + + await expect( + service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + }), + ).rejects.toThrow('PKCE is required for public clients'); + }); + }); + + describe('getAuthorizationSession with CIMD', () => { + it('should return session details for CIMD client', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + ...pkceParams, + }); + + const details = await service.getAuthorizationSession({ + sessionId: authSession.id, + }); + + expect(details).toEqual( + expect.objectContaining({ + id: authSession.id, + clientId: cimdClientId, + clientName: 'CIMD Test Client', + redirectUri: 'http://localhost:8080/callback', + scope: 'openid', + state: 'test-state', + }), + ); + }); + }); + + describe('full CIMD authorization flow', () => { + it('should complete full authorization flow for CIMD client', async () => { + const { service, mocks } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + scope: 'openid', + ...pkceParams, + }); + + const approveResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + expect(approveResult.redirectUrl).toMatch( + /^http:\/\/localhost:8080\/callback\?code=.+$/, + ); + + const code = new URL(approveResult.redirectUrl).searchParams.get( + 'code', + )!; + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'http://localhost:8080/callback', + grantType: 'authorization_code', + codeVerifier: pkceCodeVerifier, + }); + + expect(tokenResult).toEqual({ + accessToken: mockToken, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: mockToken, + scope: 'openid', + }); + }); + + it('should complete CIMD flow with PKCE', async () => { + const { service, mocks } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + }, + }, + }); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const codeVerifier = 'test-code-verifier-for-pkce'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + // Create authorization session with PKCE + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + // Approve the session + const approveResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + // Exchange code for token with verifier + const code = new URL(approveResult.redirectUrl).searchParams.get( + 'code', + )!; + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'http://localhost:8080/callback', + grantType: 'authorization_code', + codeVerifier, + }); + + expect(tokenResult.accessToken).toBe(mockToken); + }); + }); + + describe('coexistence of CIMD and DCR', () => { + it('should use DCR for non-URL client_id when both are enabled', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + experimentalDynamicClientRegistration: { enabled: true }, + }, + }, + }); + + // Register a DCR client + const dcrClient = await service.registerClient({ + clientName: 'DCR Client', + redirectUris: ['https://example.com/callback'], + }); + + // Create session with DCR client + const authSession = await service.createAuthorizationSession({ + clientId: dcrClient.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + expect(authSession.clientName).toBe('DCR Client'); + expect(mockFetchCimdMetadata).not.toHaveBeenCalled(); + }); + + it('should use CIMD for URL client_id when both are enabled', async () => { + const { service } = await createOidcService({ + databaseId, + config: { + auth: { + experimentalClientIdMetadataDocuments: { enabled: true }, + experimentalDynamicClientRegistration: { enabled: true }, + }, + }, + }); + + const authSession = await service.createAuthorizationSession({ + clientId: cimdClientId, + redirectUri: 'http://localhost:8080/callback', + responseType: 'code', + ...pkceParams, + }); + + expect(authSession.clientName).toBe('CIMD Test Client'); + expect(mockFetchCimdMetadata).toHaveBeenCalledWith({ + clientId: cimdClientId, + validatedUrl: expect.any(URL), + }); + }); + }); }); }); }); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 991cc2bcb9..5943f7dd8d 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthService, RootConfigService } from '@backstage/backend-plugin-api'; +import { + AuthService, + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { @@ -27,7 +31,19 @@ import { OidcDatabase } from '../database/OidcDatabase'; import { DateTime } from 'luxon'; import matcher from 'matcher'; import { OfflineAccessService } from './OfflineAccessService'; -import { readDcrTokenExpiration } from './readTokenExpiration'; +import { validateCimdUrl, fetchCimdMetadata } from './CimdClient'; + +function validateRedirectUri( + redirectUri: string, + allowedPatterns: string[], +): void { + const parsed = new URL(redirectUri); + const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`; + + if (!allowedPatterns.some(pattern => matcher.isMatch(normalized, pattern))) { + throw new InputError('Invalid redirect_uri'); + } +} export class OidcService { private readonly auth: AuthService; @@ -36,6 +52,7 @@ export class OidcService { private readonly userInfo: UserInfoDatabase; private readonly oidc: OidcDatabase; private readonly config: RootConfigService; + private readonly logger: LoggerService; private readonly offlineAccess?: OfflineAccessService; private constructor( @@ -45,6 +62,7 @@ export class OidcService { userInfo: UserInfoDatabase, oidc: OidcDatabase, config: RootConfigService, + logger: LoggerService, offlineAccess?: OfflineAccessService, ) { this.auth = auth; @@ -53,6 +71,7 @@ export class OidcService { this.userInfo = userInfo; this.oidc = oidc; this.config = config; + this.logger = logger; this.offlineAccess = offlineAccess; } @@ -63,6 +82,7 @@ export class OidcService { userInfo: UserInfoDatabase; oidc: OidcDatabase; config: RootConfigService; + logger: LoggerService; offlineAccess?: OfflineAccessService; }) { return new OidcService( @@ -72,6 +92,7 @@ export class OidcService { options.userInfo, options.oidc, options.config, + options.logger, options.offlineAccess, ); } @@ -80,6 +101,7 @@ export class OidcService { const dcrEnabled = this.config.getOptionalBoolean( 'auth.experimentalDynamicClientRegistration.enabled', ); + const { enabled: cimdEnabled } = this.getCimdConfig(); return { issuer: this.baseUrl, @@ -107,6 +129,7 @@ export class OidcService { token_endpoint_auth_methods_supported: [ 'client_secret_basic', 'client_secret_post', + ...(cimdEnabled ? ['none'] : []), ], claims_supported: ['sub', 'ent'], grant_types_supported: [ @@ -119,6 +142,7 @@ export class OidcService { registration_endpoint: `${this.baseUrl}/v1/register`, revocation_endpoint: `${this.baseUrl}/v1/revoke`, }), + ...(cimdEnabled && { client_id_metadata_document_supported: true }), }; } @@ -159,13 +183,7 @@ export class OidcService { ) ?? ['*']; for (const redirectUri of opts.redirectUris ?? []) { - if ( - !allowedRedirectUriPatterns.some(pattern => - matcher.isMatch(redirectUri, pattern), - ) - ) { - throw new InputError('Invalid redirect_uri'); - } + validateRedirectUri(redirectUri, allowedRedirectUriPatterns); } return await this.oidc.createClient({ @@ -204,7 +222,13 @@ export class OidcService { throw new InputError('Only authorization code flow is supported'); } - const client = await this.resolveClient(clientId, redirectUri); + const client = await this.resolveClient({ clientId, redirectUri }); + + if (client.requiresPkce && !codeChallenge) { + throw new InputError( + 'PKCE is required for public clients. Provide a code_challenge parameter.', + ); + } if (codeChallenge) { if ( @@ -239,42 +263,99 @@ export class OidcService { }; } - private async getClientName(clientId: string): Promise { - const client = await this.oidc.getClient({ clientId }); - if (!client) { - throw new InputError('Invalid client_id'); - } - return client.clientName; + private getCimdConfig() { + return { + enabled: + this.config.getOptionalBoolean( + 'auth.experimentalClientIdMetadataDocuments.enabled', + ) ?? false, + allowedClientIdPatterns: this.config.getOptionalStringArray( + 'auth.experimentalClientIdMetadataDocuments.allowedClientIdPatterns', + ) ?? ['*'], + allowedRedirectUriPatterns: this.config.getOptionalStringArray( + 'auth.experimentalClientIdMetadataDocuments.allowedRedirectUriPatterns', + ) ?? ['*'], + }; } - private async resolveClient( - clientId: string, - redirectUri: string, - ): Promise<{ clientName: string; redirectUris: string[] }> { - const client = await this.oidc.getClient({ clientId }); + private async resolveClient(opts: { + clientId: string; + redirectUri?: string; + }) { + let cimdUrl: URL | undefined; + try { + cimdUrl = validateCimdUrl(opts.clientId); + } catch { + // Not a valid CIMD URL, fall through to DCR + } + + if (cimdUrl) { + return this.resolveCimdClient({ ...opts, cimdUrl }); + } + return this.resolveDcrClient(opts); + } + + private async resolveCimdClient(opts: { + clientId: string; + cimdUrl: URL; + redirectUri?: string; + }) { + const cimd = this.getCimdConfig(); + + if (!cimd.enabled) { + throw new InputError('Client ID metadata documents not enabled'); + } + + if ( + !cimd.allowedClientIdPatterns.some(pattern => + matcher.isMatch(opts.clientId, pattern), + ) + ) { + throw new InputError('Invalid client_id'); + } + + const cimdClient = await fetchCimdMetadata({ + clientId: opts.clientId, + validatedUrl: opts.cimdUrl, + }); + + if (opts.redirectUri) { + validateRedirectUri(opts.redirectUri, cimd.allowedRedirectUriPatterns); + + if (!cimdClient.redirectUris.includes(opts.redirectUri)) { + throw new InputError('Redirect URI not registered'); + } + } + + return { + clientName: cimdClient.clientName, + redirectUris: cimdClient.redirectUris, + requiresPkce: true, + }; + } + + private async resolveDcrClient(opts: { + clientId: string; + redirectUri?: string; + }) { + const client = await this.oidc.getClient({ clientId: opts.clientId }); if (!client) { throw new InputError('Invalid client_id'); } - if (!client.redirectUris.includes(redirectUri)) { + if (opts.redirectUri && !client.redirectUris.includes(opts.redirectUri)) { throw new InputError('Invalid redirect_uri'); } return { clientName: client.clientName, redirectUris: client.redirectUris, + requiresPkce: false, }; } - public async approveAuthorizationSession(opts: { - sessionId: string; - userEntityRef: string; - }) { - const { sessionId, userEntityRef } = opts; - - const session = await this.oidc.getAuthorizationSession({ - id: sessionId, - }); + private async getValidPendingSession(sessionId: string) { + const session = await this.oidc.getAuthorizationSession({ id: sessionId }); if (!session) { throw new NotFoundError('Invalid authorization session'); @@ -288,6 +369,16 @@ export class OidcService { throw new NotFoundError('Authorization session not found or expired'); } + return session; + } + + public async approveAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + const session = await this.getValidPendingSession(sessionId); + await this.oidc.updateAuthorizationSession({ id: session.id, userEntityRef, @@ -316,24 +407,11 @@ export class OidcService { } public async getAuthorizationSession(opts: { sessionId: string }) { - const session = await this.oidc.getAuthorizationSession({ - id: opts.sessionId, + const session = await this.getValidPendingSession(opts.sessionId); + const { clientName } = await this.resolveClient({ + clientId: session.clientId, }); - if (!session) { - throw new NotFoundError('Invalid authorization session'); - } - - if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { - throw new InputError('Authorization session expired'); - } - - if (session.status !== 'pending') { - throw new NotFoundError('Authorization session not found or expired'); - } - - const clientName = await this.getClientName(session.clientId); - return { id: session.id, clientId: session.clientId, @@ -355,22 +433,7 @@ export class OidcService { userEntityRef: string; }) { const { sessionId, userEntityRef } = opts; - - const session = await this.oidc.getAuthorizationSession({ - id: sessionId, - }); - - if (!session) { - throw new NotFoundError('Invalid authorization session'); - } - - if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { - throw new InputError('Authorization session expired'); - } - - if (session.status !== 'pending') { - throw new NotFoundError('Authorization session not found or expired'); - } + const session = await this.getValidPendingSession(sessionId); await this.oidc.updateAuthorizationSession({ id: session.id, @@ -384,9 +447,8 @@ export class OidcService { redirectUri: string; codeVerifier?: string; grantType: string; - expiresIn: number; }) { - const { code, redirectUri, codeVerifier, grantType, expiresIn } = params; + const { code, redirectUri, codeVerifier, grantType } = params; if (grantType !== 'authorization_code') { throw new InputError('Unsupported grant type'); @@ -431,11 +493,11 @@ export class OidcService { } if ( - !this.verifyPkce( - session.codeChallenge, + !this.verifyPkce({ + codeChallenge: session.codeChallenge, codeVerifier, - session.codeChallengeMethod, - ) + method: session.codeChallengeMethod, + }) ) { throw new AuthenticationError('Invalid code verifier'); } @@ -456,16 +518,24 @@ export class OidcService { let refreshToken: string | undefined; const scopes = session.scope?.split(' ') ?? []; if (scopes.includes('offline_access') && this.offlineAccess) { - refreshToken = await this.offlineAccess.issueRefreshToken({ - userEntityRef: session.userEntityRef, - oidcClientId: session.clientId, - }); + try { + refreshToken = await this.offlineAccess.issueRefreshToken({ + userEntityRef: session.userEntityRef, + oidcClientId: session.clientId, + }); + } catch (err) { + // Don't fail the entire token exchange if refresh token issuance fails. + // The access token is still valid and should be returned. + this.logger.warn( + `Failed to issue refresh token for user ${session.userEntityRef}, offline_access will not be available: ${err}`, + ); + } } return { accessToken: token, tokenType: 'Bearer', - expiresIn: expiresIn, + expiresIn: 3600, idToken: token, scope: session.scope || 'openid', refreshToken, @@ -492,12 +562,10 @@ export class OidcService { clientId: params.clientId, }); - const expiresIn = readDcrTokenExpiration(this.config); - return { accessToken, tokenType: 'Bearer', - expiresIn, + expiresIn: 3600, refreshToken, }; } @@ -532,19 +600,21 @@ export class OidcService { await this.offlineAccess.revokeRefreshToken(token); } - private verifyPkce( - codeChallenge: string, - codeVerifier: string, - method?: string, - ): boolean { - if (!method || method === 'plain') { - return codeChallenge === codeVerifier; + private verifyPkce(opts: { + codeChallenge: string; + codeVerifier: string; + method?: string; + }): boolean { + if (!opts.method || opts.method === 'plain') { + return opts.codeChallenge === opts.codeVerifier; } - if (method === 'S256') { - const hash = crypto.createHash('sha256').update(codeVerifier).digest(); - const base64urlHash = hash.toString('base64url'); - return codeChallenge === base64urlHash; + if (opts.method === 'S256') { + const hash = crypto + .createHash('sha256') + .update(opts.codeVerifier) + .digest('base64url'); + return opts.codeChallenge === hash; } return false; diff --git a/plugins/auth-backend/src/service/readTokenExpiration.test.ts b/plugins/auth-backend/src/service/readTokenExpiration.test.ts index 69b6bbf848..56e711a944 100644 --- a/plugins/auth-backend/src/service/readTokenExpiration.test.ts +++ b/plugins/auth-backend/src/service/readTokenExpiration.test.ts @@ -15,10 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { - readBackstageTokenExpiration, - readTokenExpiration, -} from './readTokenExpiration.ts'; +import { readBackstageTokenExpiration } from './readTokenExpiration'; describe('Test for default backstage token expiry time', () => { it('Will return default backstage session expiration', () => { @@ -77,57 +74,4 @@ describe('Test for default backstage token expiry time', () => { }); expect(readBackstageTokenExpiration(config)).toBe(86400); }); - - it('will return expiration from custom key', () => { - const config = new ConfigReader({ - app: { - baseUrl: 'http://example.com/extra-path', - }, - custom: { - tokenExp: { minutes: 20 }, - }, - }); - expect(readTokenExpiration(config, { configKey: 'custom.tokenExp' })).toBe( - 1200, - ); - }); - - it('will return custom default expiration', () => { - const config = new ConfigReader({}); - expect( - readTokenExpiration(config, { - configKey: 'auth.backstageTokenExpiration', - defaultExpiration: 1234, - }), - ).toBe(1234); - }); - - it('will return custom min/max expiration', () => { - const config = new ConfigReader({ - auth: { - backstageTokenExpiration: { minutes: 20 }, - }, - }); - expect( - readTokenExpiration(config, { - configKey: 'auth.backstageTokenExpiration', - minExpiration: 2000, - maxExpiration: 3000, - }), - ).toBe(2000); - expect( - readTokenExpiration(config, { - configKey: 'auth.backstageTokenExpiration', - minExpiration: 1000, - maxExpiration: 1100, - }), - ).toBe(1100); - expect( - readTokenExpiration(config, { - configKey: 'auth.backstageTokenExpiration', - minExpiration: 1000, - maxExpiration: 2000, - }), - ).toBe(1200); - }); }); diff --git a/plugins/auth-backend/src/service/readTokenExpiration.ts b/plugins/auth-backend/src/service/readTokenExpiration.ts index 3094d2c054..c687ecdfc5 100644 --- a/plugins/auth-backend/src/service/readTokenExpiration.ts +++ b/plugins/auth-backend/src/service/readTokenExpiration.ts @@ -23,46 +23,22 @@ const TOKEN_EXP_MIN_S = 600; const TOKEN_EXP_MAX_S = 86400; export function readBackstageTokenExpiration(config: RootConfigService) { - return readTokenExpiration(config, { - configKey: 'auth.backstageTokenExpiration', - }); -} + const processingIntervalKey = 'auth.backstageTokenExpiration'; -export function readDcrTokenExpiration(config: RootConfigService) { - return readTokenExpiration(config, { - configKey: 'auth.experimentalDynamicClientRegistration.tokenExpiration', - }); -} - -export function readTokenExpiration( - config: RootConfigService, - options: { - configKey: string; - maxExpiration?: number; - minExpiration?: number; - defaultExpiration?: number; - }, -): number { - const { - configKey, - maxExpiration = TOKEN_EXP_MAX_S, - minExpiration = TOKEN_EXP_MIN_S, - defaultExpiration = TOKEN_EXP_DEFAULT_S, - } = options ?? {}; - if (!config.has(configKey)) { - return defaultExpiration; + if (!config.has(processingIntervalKey)) { + return TOKEN_EXP_DEFAULT_S; } const duration = readDurationFromConfig(config, { - key: configKey, + key: processingIntervalKey, }); const durationS = Math.round(durationToMilliseconds(duration) / 1000); - if (durationS < minExpiration) { - return minExpiration; - } else if (durationS > maxExpiration) { - return maxExpiration; + if (durationS < TOKEN_EXP_MIN_S) { + return TOKEN_EXP_MIN_S; + } else if (durationS > TOKEN_EXP_MAX_S) { + return TOKEN_EXP_MAX_S; } return durationS; } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index ec5b303710..8a8118c43e 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -35,10 +35,8 @@ import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; import passport from 'passport'; import { AuthDatabase } from '../database/AuthDatabase'; -import { - readBackstageTokenExpiration, - readDcrTokenExpiration, -} from './readTokenExpiration.ts'; +import { readBackstageTokenExpiration } from './readTokenExpiration'; +import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { bindProviderRouters, ProviderFactories } from '../providers/router'; @@ -95,37 +93,29 @@ export async function createRouter( ? ['ent'] : []; - const createTokenIssuer = (opts: { - logger: LoggerService; - expirationSeconds: number; - }) => { - if (keyStore instanceof StaticKeyStore) { - return new StaticTokenIssuer( - { - logger: opts.logger, - issuer: authUrl, - sessionExpirationSeconds: opts.expirationSeconds, - omitClaimsFromToken, - }, - keyStore as StaticKeyStore, - ); - } - return new TokenFactory({ + let tokenIssuer: TokenIssuer; + if (keyStore instanceof StaticKeyStore) { + tokenIssuer = new StaticTokenIssuer( + { + logger: logger.child({ component: 'token-factory' }), + issuer: authUrl, + sessionExpirationSeconds: backstageTokenExpiration, + omitClaimsFromToken, + }, + keyStore as StaticKeyStore, + ); + } else { + tokenIssuer = new TokenFactory({ issuer: authUrl, keyStore, - keyDurationSeconds: opts.expirationSeconds, - logger: opts.logger, + keyDurationSeconds: backstageTokenExpiration, + logger: logger.child({ component: 'token-factory' }), algorithm: tokenFactoryAlgorithm ?? config.getOptionalString('auth.identityTokenAlgorithm'), omitClaimsFromToken, }); - }; - - const tokenIssuer = createTokenIssuer({ - logger: logger.child({ component: 'token-factory' }), - expirationSeconds: backstageTokenExpiration, - }); + } const secret = config.getOptionalString('auth.session.secret'); if (secret) { @@ -163,18 +153,11 @@ export async function createRouter( userInfo, }); - const dcrTokenExpiration = readDcrTokenExpiration(config); - - const oidcTokenIssuer = createTokenIssuer({ - logger: logger.child({ component: 'oidc-token-factory' }), - expirationSeconds: dcrTokenExpiration, - }); - const oidc = await OidcDatabase.create({ database }); const oidcRouter = OidcRouter.create({ auth: options.auth, - tokenIssuer: oidcTokenIssuer, + tokenIssuer, baseUrl: authUrl, appUrl, userInfo, diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index c0997ddece..0ac9970046 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-auth-node +## 0.6.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + +## 0.6.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.6.13 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + ## 0.6.13-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 14a8675d8d..c89095dd95 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.6.13-next.1", + "version": "0.6.14-next.2", "backstage": { "role": "node-library", "pluginId": "auth", diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 474fd0f413..1b8da082e0 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-react +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + +## 0.1.24 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 204193b3be..9466e09e19 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.24-next.1", + "version": "0.1.25-next.0", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md index 5757a3796c..f5881310d5 100644 --- a/plugins/auth/CHANGELOG.md +++ b/plugins/auth/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-auth +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + +## 0.1.5 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + ## 0.1.5-next.2 ### Patch Changes diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 4d6e31d607..52a51d81ce 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth", - "version": "0.1.5-next.2", + "version": "0.1.6-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "auth", @@ -47,13 +47,10 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/core-components": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", - "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.61", + "@backstage/ui": "workspace:^", + "@remixicon/react": "^4.6.0", "react-use": "^17.2.4" }, "devDependencies": { diff --git a/plugins/auth/report.api.md b/plugins/auth/report.api.md index 1d346a9ce4..8e8ac71d31 100644 --- a/plugins/auth/report.api.md +++ b/plugins/auth/report.api.md @@ -4,7 +4,10 @@ ```ts import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -22,26 +25,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; } diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.module.css b/plugins/auth/src/components/ConsentPage/ConsentPage.module.css new file mode 100644 index 0000000000..049c512c54 --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.module.css @@ -0,0 +1,44 @@ +.card { + max-width: 600px; + margin: var(--bui-space-8) auto 0; +} + +.appHeader { + display: flex; + align-items: center; + gap: var(--bui-space-4); +} + +.appIcon { + color: var(--bui-fg-secondary); +} + +.divider { + border: none; + border-top: 1px solid var(--bui-border-1); + margin: 0; +} + +.callbackUrl { + font-family: var(--bui-font-monospace); + background: var(--bui-bg-neutral-2); + padding: var(--bui-space-2); + border-radius: var(--bui-radius-2); + word-break: break-all; + font-size: var(--bui-font-size-3); + margin-top: var(--bui-space-2); +} + +.completedIcon { + margin-bottom: var(--bui-space-4); +} + +.completedIconSuccess { + composes: completedIcon; + color: var(--bui-fg-success); +} + +.completedIconDanger { + composes: completedIcon; + color: var(--bui-fg-danger); +} diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx index 859d82fe04..59e7b512a5 100644 --- a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx @@ -16,89 +16,37 @@ import { useParams } from 'react-router-dom'; import { + Alert, Box, Button, Card, - CardActions, - CardContent, - Divider, - makeStyles, - Typography, -} from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import CheckCircleIcon from '@material-ui/icons/CheckCircle'; -import CancelIcon from '@material-ui/icons/Cancel'; -import AppsIcon from '@material-ui/icons/Apps'; -import WarningIcon from '@material-ui/icons/Warning'; + CardBody, + CardFooter, + Container, + Flex, + FullPage, + Text, + VisuallyHidden, +} from '@backstage/ui'; import { - Content, - EmptyState, - Header, - Page, - Progress, - ResponseErrorPanel, -} from '@backstage/core-components'; + RiAppsLine, + RiCheckboxCircleLine, + RiCloseCircleLine, +} from '@remixicon/react'; import { useConsentSession } from './useConsentSession'; import { configApiRef, useApi } from '@backstage/frontend-plugin-api'; +import styles from './ConsentPage.module.css'; -const useStyles = makeStyles(theme => ({ - authCard: { - maxWidth: 600, - margin: '0 auto', - marginTop: theme.spacing(4), - }, - appHeader: { - display: 'flex', - alignItems: 'center', - marginBottom: theme.spacing(2), - }, - appIcon: { - marginRight: theme.spacing(2), - fontSize: 40, - }, - appName: { - fontSize: '1.5rem', - fontWeight: 'bold', - }, - securityWarning: { - margin: theme.spacing(2, 0), - }, - buttonContainer: { - display: 'flex', - justifyContent: 'space-between', - gap: theme.spacing(2), - padding: theme.spacing(2), - }, - callbackUrl: { - fontFamily: 'monospace', - backgroundColor: theme.palette.background.default, - padding: theme.spacing(1), - borderRadius: theme.shape.borderRadius, - wordBreak: 'break-all', - fontSize: '0.875rem', - }, - scopeList: { - backgroundColor: theme.palette.background.default, - borderRadius: theme.shape.borderRadius, - padding: theme.spacing(1), - }, -})); - -const ConsentPageLayout = ({ - title, - children, -}: { - title: string; - children: React.ReactNode; -}) => ( - -
    - {children} - +const ConsentPageLayout = ({ children }: { children: React.ReactNode }) => ( + + +

    Authorization

    +
    + {children} +
    ); export const ConsentPage = () => { - const classes = useStyles(); const { sessionId } = useParams<{ sessionId: string }>(); const { state, handleAction } = useConsentSession({ sessionId }); const configApi = useApi(configApiRef); @@ -106,9 +54,10 @@ export const ConsentPage = () => { if (!sessionId) { return ( - - + @@ -118,57 +67,62 @@ export const ConsentPage = () => { if (state.status === 'loading') { return ( - - - - + + ); } if (state.status === 'error') { return ( - - + + ); } if (state.status === 'completed') { return ( - - - - + + + + {state.action === 'approve' ? ( - ) : ( - )} - + {state.action === 'approve' ? 'Authorization Approved' : 'Authorization Denied'} - - + + {state.action === 'approve' ? `You have successfully authorized the application to access your ${appTitle} account.` : `You have denied the application access to your ${appTitle} account.`} - - + + Redirecting to the application... - - - + + + ); @@ -179,70 +133,67 @@ export const ConsentPage = () => { const appName = session.clientName ?? session.clientId; return ( - - - - - - - {appName} - - wants to access your {appTitle} account - + + + + + + + + + {appName} + + + wants to access your {appTitle} account + + - - +
    - } - className={classes.securityWarning} - > - - Security Notice: By authorizing this application, - you are granting it access to your {appTitle} account. The - application will receive an access token that allows it to act on - your behalf. - - - - Callback URL: - - {session.redirectUri} - - + + By authorizing this application, you are granting it access to + your {appTitle} account. The application will receive an + access token that allows it to act on your behalf. +
    + {session.redirectUri} +
    + + } + /> - - + Make sure you trust this application and recognize the callback URL above. Only authorize applications you trust. - - -
    + + + - - - - + + + + + +
    ); diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index 9d96cc0c12..721c7247cf 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + +## 0.3.7 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 27fa6f9f00..010a284f60 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.3.7-next.1", + "version": "0.3.8-next.1", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index f902defc62..9323735454 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.4.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## 0.4.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## 0.4.20 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.4.20-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 54a134d38d..8acb4f8934 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.20-next.2", + "version": "0.4.21-next.2", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index be1a0c885a..fa73188213 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -179,7 +179,7 @@ export class AwsS3EntityProvider implements EntityProvider { { Bucket: bucketName, }, - ListObjectsV2Command, + ListObjectsV2Command as any, this.s3.config as unknown as Record, ); if (endpoint?.url) diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index a1f084ac52..aff863a093 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.3.14 + +### Patch Changes + +- cc6206e: Added support for `{org}.visualstudio.com` domains used by Azure DevOps +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 6c8a464: Added missing `branch` field to the `azureDevOps` provider config schema. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.3.14-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/config.d.ts b/plugins/catalog-backend-module-azure/config.d.ts index 0eb488734f..cb7c6c5594 100644 --- a/plugins/catalog-backend-module-azure/config.d.ts +++ b/plugins/catalog-backend-module-azure/config.d.ts @@ -44,6 +44,10 @@ export interface Config { * If not set, all repositories will be searched. */ repository?: string; + /** + * (Optional) The name of a branch to use. If not set, defaults to the default branch of the repository. + */ + branch?: string; /** * (Optional) Where to find catalog-info.yaml files. Wildcards are supported. * If not set, defaults to /catalog-info.yaml. diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 35e7fa6c6d..9c8de87ffb 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.14-next.2", + "version": "0.3.15-next.2", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index e692be66e3..1281306c0e 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -103,7 +103,12 @@ describe('AzureDevOpsDiscoveryProcessor', () => { const processor = AzureDevOpsDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { - azure: [{ host: 'dev.azure.com', token: 'blob' }], + azure: [ + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'token' }], + }, + ], }, }), { logger: mockServices.logger.mock() }, @@ -123,8 +128,14 @@ describe('AzureDevOpsDiscoveryProcessor', () => { new ConfigReader({ integrations: { github: [ - { host: 'dev.azure.com', token: 'blob' }, - { host: 'azure.myorg.com', token: 'blob' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'token' }], + }, + { + host: 'azure.myorg.com', + credentials: [{ personalAccessToken: 'token' }], + }, ], }, }), @@ -145,7 +156,12 @@ describe('AzureDevOpsDiscoveryProcessor', () => { const processor = AzureDevOpsDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { - github: [{ host: 'dev.azure.com', token: 'blob' }], + github: [ + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'token' }], + }, + ], }, }), { logger: mockServices.logger.mock() }, diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index e69bea9293..228d8fff1b 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.5.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.5.11 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + ## 0.5.11-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index 50f324ac93..078f916d82 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.5.11-next.1", + "version": "0.5.12-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index c8e55a5f0a..d1aa89375b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.8 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + ## 0.5.8-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index c456c5f486..5d05540e02 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.8-next.2", + "version": "0.5.9-next.2", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index faf7901cf5..0a051ef082 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.8 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + ## 0.5.8-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index ccaedbd618..4f310f203e 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.5.8-next.2", + "version": "0.5.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 0f1ec87037..f90856a15f 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-kubernetes-common@0.9.10 + +## 0.3.16 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-common@0.9.10 + ## 0.3.16-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index bb4f6ff0f0..149984105a 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.3.16-next.1", + "version": "0.3.17-next.1", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gcp/report.api.md b/plugins/catalog-backend-module-gcp/report.api.md index 6260d07c88..784de87d2e 100644 --- a/plugins/catalog-backend-module-gcp/report.api.md +++ b/plugins/catalog-backend-module-gcp/report.api.md @@ -20,22 +20,13 @@ export class GkeEntityProvider implements EntityProvider { // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) - static fromConfig({ - logger, - scheduler, - config, - }: { + static fromConfig(input: { logger: LoggerService; scheduler: SchedulerService; config: Config; }): GkeEntityProvider; // (undocumented) - static fromConfigWithClient({ - logger, - scheduler, - config, - clusterManagerClient, - }: { + static fromConfigWithClient(input: { logger: LoggerService; scheduler: SchedulerService; config: Config; diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index d3def2c8b0..f82a571f61 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.3.11 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.3.11-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 0363e8309c..59514c6821 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.3.11-next.2", + "version": "0.3.12-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gitea/CHANGELOG.md b/plugins/catalog-backend-module-gitea/CHANGELOG.md index 885ec39acb..c5e64ef6e1 100644 --- a/plugins/catalog-backend-module-gitea/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-catalog-backend-module-gitea +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.1.9 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.1.9-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitea/README.md b/plugins/catalog-backend-module-gitea/README.md index 6b93c9c90a..12c9ae923a 100644 --- a/plugins/catalog-backend-module-gitea/README.md +++ b/plugins/catalog-backend-module-gitea/README.md @@ -4,5 +4,5 @@ This is an extension module to the plugin-catalog-backend plugin, providing exte ## Getting started -See [Backstage documentation](https://backstage.io/docs/integrations/gitea/discovery.md) +See [Backstage documentation](https://backstage.io/docs/integrations/gitea/discovery) for details on how to install and configure the plugin. diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 34eb2bad54..656ddc8b46 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.9-next.2", + "version": "0.1.10-next.2", "description": "The gitea backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts index d6a31899f1..539d432793 100644 --- a/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitea/src/providers/GiteaEntityProvider.test.ts @@ -34,6 +34,7 @@ describe('GiteaEntityProvider', () => { triggerTask: jest.fn(), scheduleTask: jest.fn(), getScheduledTasks: jest.fn(), + cancelTask: jest.fn(), }; const mockTaskRunner = { run: jest.fn(), diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index 5708122a53..a380b637ee 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.20-next.2 + +### Patch Changes + +- 106d1b2: Added a `defaultUserTransformer.useVerifiedEmails` config option for the `githubOrg` provider. When set to `true`, the default user transformer prefers organization verified domain emails over the user's public GitHub email. Defaults to `false`, which uses only the public GitHub email. + + This option has no effect when a custom user transformer is set via the `githubOrgEntityProviderTransformsExtensionPoint`. + + ```yaml + catalog: + providers: + githubOrg: + production: + githubUrl: https://github.com + orgs: + - my-org + defaultUserTransformer: + useVerifiedEmails: true + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.13.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.3.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.3.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.3-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.3.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.2 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.3.19-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index df3c7f7b51..24c9e3361e 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.19-next.1", + "version": "0.3.20-next.2", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github-org/src/module.ts b/plugins/catalog-backend-module-github-org/src/module.ts index 137bd84ef7..08a557e3b1 100644 --- a/plugins/catalog-backend-module-github-org/src/module.ts +++ b/plugins/catalog-backend-module-github-org/src/module.ts @@ -23,6 +23,7 @@ import { } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { + buildDefaultUserTransformer, GithubMultiOrgEntityProvider, TeamTransformer, UserTransformer, @@ -116,7 +117,11 @@ export const catalogModuleGithubOrgEntityProvider = createBackendModule({ definition.schedule, ), logger, - userTransformer, + userTransformer: + userTransformer ?? + buildDefaultUserTransformer({ + useVerifiedEmails: definition.useVerifiedEmails, + }), teamTransformer, alwaysUseDefaultNamespace: definitions.length === 1 && definition.orgs?.length === 1, @@ -141,6 +146,7 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ organizationMembers?: number; }; excludeSuspendedUsers?: boolean; + useVerifiedEmails?: boolean; }> { const baseKey = 'catalog.providers.githubOrg'; const baseConfig = rootConfig.getOptional(baseKey); @@ -170,5 +176,7 @@ function readDefinitionsFromConfig(rootConfig: Config): Array<{ : undefined, excludeSuspendedUsers: c.getOptionalBoolean('excludeSuspendedUsers') ?? false, + useVerifiedEmails: + c.getOptionalBoolean('defaultUserTransformer.useVerifiedEmails') ?? false, })); } diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index bdeff51df5..7743d3cfe8 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,109 @@ # @backstage/plugin-catalog-backend-module-github +## 0.13.0-next.2 + +### Patch Changes + +- 106d1b2: Added a `defaultUserTransformer.useVerifiedEmails` config option for the `githubOrg` provider. When set to `true`, the default user transformer prefers organization verified domain emails over the user's public GitHub email. Defaults to `false`, which uses only the public GitHub email. + + This option has no effect when a custom user transformer is set via the `githubOrgEntityProviderTransformsExtensionPoint`. + + ```yaml + catalog: + providers: + githubOrg: + production: + githubUrl: https://github.com + orgs: + - my-org + defaultUserTransformer: + useVerifiedEmails: true + ``` + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.13.0-next.1 + +### Minor Changes + +- b11c2cd: The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails. + + If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`: + + ```ts + import { createBackendModule } from '@backstage/backend-plugin-api'; + import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org'; + import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github'; + + export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'github-org-custom-transforms', + register(env) { + env.registerInit({ + deps: { + transforms: githubOrgEntityProviderTransformsExtensionPoint, + }, + async init({ transforms }) { + transforms.setUserTransformer(async (item, ctx) => { + const entity = await defaultUserTransformer(item, ctx); + if (entity && item.email) { + entity.spec.profile!.email = item.email; + } + return entity; + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.12.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.12.2 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 34cc520: Implemented translation of webhook events into `catalogScmEventsServiceRef` events. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + ## 0.12.2-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index 5425cae9d0..6a9d5cf808 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -270,6 +270,24 @@ export interface Config { */ excludeSuspendedUsers?: boolean; + /** + * (Optional) Configuration for the default user transformer. + * These options only apply when using the built-in transformer; + * they have no effect if a custom transformer is set via the + * extension point. + */ + defaultUserTransformer?: { + /** + * (Optional) Whether to prefer organization verified domain emails + * over the user's public GitHub email when populating user entity profiles. + * When enabled, the transformer uses the first verified domain email + * (with plus-addressed routing tags stripped) and falls back to the + * public email if none are available. + * Default: `false`. + */ + useVerifiedEmails?: boolean; + }; + /** * The refresh schedule to use. */ @@ -327,6 +345,24 @@ export interface Config { */ excludeSuspendedUsers?: boolean; + /** + * (Optional) Configuration for the default user transformer. + * These options only apply when using the built-in transformer; + * they have no effect if a custom transformer is set via the + * extension point. + */ + defaultUserTransformer?: { + /** + * (Optional) Whether to prefer organization verified domain emails + * over the user's public GitHub email when populating user entity profiles. + * When enabled, the transformer uses the first verified domain email + * (with plus-addressed routing tags stripped) and falls back to the + * public email if none are available. + * Default: `false`. + */ + useVerifiedEmails?: boolean; + }; + /** * The refresh schedule to use. */ diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 49ecf76f57..a23d09141f 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.12.2-next.2", + "version": "0.13.0-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", @@ -68,7 +68,7 @@ "@octokit/webhooks-types": "^7.6.1", "git-url-parse": "^15.0.0", "lodash": "^4.17.21", - "minimatch": "^9.0.0", + "minimatch": "^10.2.1", "octokit": "^3.0.0", "uuid": "^11.0.0" }, diff --git a/plugins/catalog-backend-module-github/report.api.md b/plugins/catalog-backend-module-github/report.api.md index 25fc58be79..a2642bb1f4 100644 --- a/plugins/catalog-backend-module-github/report.api.md +++ b/plugins/catalog-backend-module-github/report.api.md @@ -25,16 +25,22 @@ import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node'; -import { UserEntity } from '@backstage/catalog-model'; + +// @public +export function buildDefaultUserTransformer( + options?: DefaultUserTransformerOptions, +): UserTransformer; // @public export const defaultOrganizationTeamTransformer: TeamTransformer; // @public -export const defaultUserTransformer: ( - item: GithubUser, - _ctx: TransformerContext, -) => Promise; +export const defaultUserTransformer: UserTransformer; + +// @public +export interface DefaultUserTransformerOptions { + useVerifiedEmails?: boolean; +} // @public const githubCatalogModule: BackendFeature; diff --git a/plugins/catalog-backend-module-github/src/index.ts b/plugins/catalog-backend-module-github/src/index.ts index 6d62a0a64a..2d02d237dc 100644 --- a/plugins/catalog-backend-module-github/src/index.ts +++ b/plugins/catalog-backend-module-github/src/index.ts @@ -37,6 +37,8 @@ export { type GithubUser, type UserTransformer, defaultUserTransformer, + buildDefaultUserTransformer, + type DefaultUserTransformerOptions, type TeamTransformer, defaultOrganizationTeamTransformer, type TransformerContext, diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts new file mode 100644 index 0000000000..950906e35a --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright 2026 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 { UserEntity } from '@backstage/catalog-model'; +import { graphql } from '@octokit/graphql'; +import { + buildDefaultUserTransformer, + defaultUserTransformer, + TransformerContext, +} from './defaultTransformers'; +import { GithubUser } from './github'; + +const ctx: TransformerContext = { + client: graphql, + query: '', + org: 'test-org', +}; + +function makeUser(overrides: Partial = {}): GithubUser { + return { + login: 'testuser', + avatarUrl: '', + ...overrides, + }; +} + +describe('defaultUserTransformer', () => { + it('populates all fields correctly', async () => { + const result = (await defaultUserTransformer( + makeUser({ + name: 'Test User', + email: 'test@example.com', + bio: 'A test bio', + avatarUrl: 'https://example.com/avatar.png', + id: 'user-id-123', + }), + ctx, + )) as UserEntity; + + expect(result.metadata.name).toBe('testuser'); + expect(result.metadata.description).toBe('A test bio'); + expect(result.metadata.annotations).toEqual({ + 'github.com/user-login': 'testuser', + 'github.com/user-id': 'user-id-123', + }); + expect(result.spec.profile).toEqual({ + displayName: 'Test User', + email: 'test@example.com', + picture: 'https://example.com/avatar.png', + }); + expect(result.spec.memberOf).toEqual([]); + }); + + it('uses public email and ignores verified domain emails', async () => { + const result = (await defaultUserTransformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('sets no email when public email is absent even if verified emails exist', async () => { + const result = (await defaultUserTransformer( + makeUser({ + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBeUndefined(); + }); + + it('sets no email when both are absent', async () => { + const result = (await defaultUserTransformer( + makeUser(), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBeUndefined(); + }); +}); + +describe('buildDefaultUserTransformer', () => { + describe('with useVerifiedEmails: false', () => { + const transformer = buildDefaultUserTransformer({ + useVerifiedEmails: false, + }); + + it('uses public email and ignores verified domain emails', async () => { + const result = (await transformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('sets no email when public email is absent', async () => { + const result = (await transformer( + makeUser({ + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBeUndefined(); + }); + }); + + describe('with useVerifiedEmails: true', () => { + const transformer = buildDefaultUserTransformer({ + useVerifiedEmails: true, + }); + + it('prefers verified domain email over public email', async () => { + const result = (await transformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('corp@company.com'); + }); + + it('strips plus-addressed tag from verified domain email', async () => { + const result = (await transformer( + makeUser({ + organizationVerifiedDomainEmails: ['amckay+2jc29kv2@spotify.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('amckay@spotify.com'); + }); + + it('uses verified domain email when public email is absent', async () => { + const result = (await transformer( + makeUser({ + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('corp@company.com'); + }); + + it('falls back to public email when verified array is empty', async () => { + const result = (await transformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: [], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('falls back to public email when verified array is undefined', async () => { + const result = (await transformer( + makeUser({ + email: 'public@gmail.com', + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + + it('sets no email when both are absent', async () => { + const result = (await transformer(makeUser(), ctx)) as UserEntity; + + expect(result.spec.profile!.email).toBeUndefined(); + }); + }); + + describe('with no options', () => { + const transformer = buildDefaultUserTransformer(); + + it('behaves the same as useVerifiedEmails: false', async () => { + const result = (await transformer( + makeUser({ + email: 'public@gmail.com', + organizationVerifiedDomainEmails: ['corp@company.com'], + }), + ctx, + )) as UserEntity; + + expect(result.spec.profile!.email).toBe('public@gmail.com'); + }); + }); +}); diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index 4de9af89c8..34904a59dc 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -54,37 +54,74 @@ export type TeamTransformer = ( ctx: TransformerContext, ) => Promise; +/** + * Options for {@link buildDefaultUserTransformer}. + * + * @public + */ +export interface DefaultUserTransformerOptions { + /** + * Whether to prefer organization verified domain emails over the user's + * public GitHub email. When enabled, the transformer uses the first + * verified domain email (with plus-addressed routing tags stripped) and + * falls back to the public email if none are available. + * + * @defaultValue false + */ + useVerifiedEmails?: boolean; +} + +/** + * Builds a user transformer with configurable email behavior. + * + * @public + */ +export function buildDefaultUserTransformer( + options?: DefaultUserTransformerOptions, +): UserTransformer { + return async (item, _ctx) => { + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: item.login, + annotations: { + [ANNOTATION_GITHUB_USER_LOGIN]: item.login, + ...(item.id && { [ANNOTATION_GITHUB_USER_ID]: item.id }), + }, + }, + spec: { + profile: {}, + memberOf: [], + }, + }; + + if (item.bio) entity.metadata.description = item.bio; + if (item.name) entity.spec.profile!.displayName = item.name; + + if (options?.useVerifiedEmails) { + // GitHub returns verified domain emails as plus-addressed routing aliases + // (e.g. user+abc123@example.com). Strip the tag to get the real address. + const email = item.organizationVerifiedDomainEmails?.length + ? item.organizationVerifiedDomainEmails[0].replace(/\+[^@]*/, '') + : item.email; + if (email) entity.spec.profile!.email = email; + } else { + if (item.email) entity.spec.profile!.email = item.email; + } + + if (item.avatarUrl) entity.spec.profile!.picture = item.avatarUrl; + return entity; + }; +} + /** * Default transformer for GitHub users to UserEntity * * @public */ -export const defaultUserTransformer = async ( - item: GithubUser, - _ctx: TransformerContext, -): Promise => { - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: item.login, - annotations: { - [ANNOTATION_GITHUB_USER_LOGIN]: item.login, - ...(item.id && { [ANNOTATION_GITHUB_USER_ID]: item.id }), - }, - }, - spec: { - profile: {}, - memberOf: [], - }, - }; - - if (item.bio) entity.metadata.description = item.bio; - if (item.name) entity.spec.profile!.displayName = item.name; - if (item.email) entity.spec.profile!.email = item.email; - if (item.avatarUrl) entity.spec.profile!.picture = item.avatarUrl; - return entity; -}; +export const defaultUserTransformer: UserTransformer = + buildDefaultUserTransformer(); /** * Default transformer for GitHub Team to GroupEntity diff --git a/plugins/catalog-backend-module-github/src/lib/index.ts b/plugins/catalog-backend-module-github/src/lib/index.ts index 48050e96bb..8806604888 100644 --- a/plugins/catalog-backend-module-github/src/lib/index.ts +++ b/plugins/catalog-backend-module-github/src/lib/index.ts @@ -28,6 +28,8 @@ export { export { type UserTransformer, defaultUserTransformer, + buildDefaultUserTransformer, + type DefaultUserTransformerOptions, type TeamTransformer, defaultOrganizationTeamTransformer, type TransformerContext, diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index d4199d1748..9bc20d0800 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.2.18 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.8.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 827a2795e4..d0ea816178 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.18-next.1", + "version": "0.2.19-next.1", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 64db1024d6..c172c471ed 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.8.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.8.1-next.0 + +### Patch Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.8.0 + +### Minor Changes + +- 2f51676: allow entity discoverability via gitlab search API +- ff07934: Added the `{gitlab-integration-host}/user-id` annotation to store GitLab's user ID (immutable) in user entities. Also includes addition of the `userIdMatchingUserEntityAnnotation` sign-in resolver that matches users by the new ID. + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 7e6b5e5: Fixed GitLab search API scope parameter from `'blob'` to `'blobs'`, resolving 400 errors in discovery provider. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + ## 0.8.0-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 74a4c608e3..e7b562c4e5 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.8.0-next.2", + "version": "0.8.1-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index 5ae71dda06..eda2bca1ab 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -102,7 +102,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { } const client = new GitLabClient({ - config: integration.config, + integration: integration, logger: this.logger, }); this.logger.debug(`Reading GitLab projects from ${location.target}`); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 5e10fbffd6..e4d62049c0 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -19,7 +19,10 @@ import { registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { readGitLabIntegrationConfig } from '@backstage/integration'; +import { + GitLabIntegration, + readGitLabIntegrationConfig, +} from '@backstage/integration'; import { setupServer } from 'msw/node'; import { handlers } from '../__testUtils__/handlers'; import * as mock from '../__testUtils__/mocks'; @@ -33,8 +36,10 @@ describe('GitLabClient', () => { describe('isSelfManaged', () => { it('returns true if self managed instance', () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -42,7 +47,9 @@ describe('GitLabClient', () => { }); it('returns false if gitlab.com', () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + ), logger: mockServices.logger.mock(), }); expect(client.isSelfManaged()).toBeFalsy(); @@ -52,8 +59,10 @@ describe('GitLabClient', () => { describe('pagedRequest', () => { it('should provide immediate items within the page', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -65,8 +74,10 @@ describe('GitLabClient', () => { it('should request items for a given page number', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -86,8 +97,10 @@ describe('GitLabClient', () => { it('should not have a next page if at the end', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -105,8 +118,10 @@ describe('GitLabClient', () => { it('should throw if response is not okay', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -121,8 +136,10 @@ describe('GitLabClient', () => { describe('listProjects', () => { it('should get projects for a given group', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -145,8 +162,10 @@ describe('GitLabClient', () => { it('should get not archived projects', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -170,8 +189,10 @@ describe('GitLabClient', () => { it('should get all projects for an instance', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -190,8 +211,10 @@ describe('GitLabClient', () => { it('should pass simple parameter to API when provided', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -212,8 +235,10 @@ describe('GitLabClient', () => { it('should pass simple parameter to group projects API when provided', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -241,8 +266,10 @@ describe('GitLabClient', () => { describe('listUsers', () => { it('listUsers gets all users in the instance', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -262,8 +289,10 @@ describe('GitLabClient', () => { describe('listGroups', () => { it('listGroups gets all groups in the instance', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -283,8 +312,10 @@ describe('GitLabClient', () => { describe('listFiles', () => { it('should call group search API with correct scope parameter', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -312,8 +343,10 @@ describe('GitLabClient', () => { it('should return empty items when group is missing', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -327,8 +360,10 @@ describe('GitLabClient', () => { it('should return empty items when search is missing', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -344,7 +379,9 @@ describe('GitLabClient', () => { describe('get gitlab.com users', () => { it('gets all users under group', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + ), logger: mockServices.logger.mock(), }); const saasMembers = ( @@ -358,7 +395,9 @@ describe('GitLabClient', () => { }); it('gets all users with token without full permissions', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + ), logger: mockServices.logger.mock(), }); const saasMembers = ( @@ -368,7 +407,9 @@ describe('GitLabClient', () => { }); it('rejects when GraphQL returns errors', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_saas)), + ), logger: mockServices.logger.mock(), }); await expect(() => @@ -379,8 +420,10 @@ describe('GitLabClient', () => { }); it('traverses multi-page results', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -397,8 +440,10 @@ describe('GitLabClient', () => { describe('listDescendantGroups', () => { it('gets all groups under root', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -412,8 +457,10 @@ describe('GitLabClient', () => { it('gets all descendant groups with token without full permissions', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -427,8 +474,10 @@ describe('GitLabClient', () => { it('rejects when GraphQL returns errors', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -441,8 +490,10 @@ describe('GitLabClient', () => { }); it('traverses multi-page results', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -458,8 +509,10 @@ describe('GitLabClient', () => { describe('getGroupMembers', () => { it('gets member IDs', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -481,8 +534,10 @@ describe('GitLabClient', () => { it('gets member IDs with token without full permissions', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -497,8 +552,10 @@ describe('GitLabClient', () => { // TODO: is this one really necessary? it('rejects when GraphQL returns errors', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -512,8 +569,10 @@ describe('GitLabClient', () => { it('traverses multi-page results', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -528,8 +587,10 @@ describe('GitLabClient', () => { describe('getGroupById', () => { it('should return group details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -540,8 +601,10 @@ describe('GitLabClient', () => { it('should handle errors when fetching group details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -555,8 +618,10 @@ describe('GitLabClient', () => { describe('getProjectById', () => { it('should return project details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -567,8 +632,10 @@ describe('GitLabClient', () => { it('should handle errors when fetching project details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -582,8 +649,10 @@ describe('GitLabClient', () => { describe('getUserById', () => { it('should return user details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -594,8 +663,10 @@ describe('GitLabClient', () => { it('should handle errors when fetching user details by ID', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), ), logger: mockServices.logger.mock(), }); @@ -610,8 +681,8 @@ describe('GitLabClient', () => { describe('paginated', () => { it('should iterate through the pages until exhausted', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); @@ -634,8 +705,8 @@ describe('hasFile', () => { beforeEach(() => { client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); @@ -655,8 +726,8 @@ describe('hasFile', () => { describe('pagedRequest search params', () => { it('no search params provided', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); @@ -673,8 +744,8 @@ describe('pagedRequest search params', () => { it('defined numeric search params', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); @@ -694,8 +765,8 @@ describe('pagedRequest search params', () => { it('defined string search params', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); @@ -715,8 +786,8 @@ describe('pagedRequest search params', () => { it('defined boolean search params', async () => { const client = new GitLabClient({ - config: readGitLabIntegrationConfig( - new ConfigReader(mock.config_self_managed), + integration: new GitLabIntegration( + readGitLabIntegrationConfig(new ConfigReader(mock.config_self_managed)), ), logger: mockServices.logger.mock(), }); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index c04585142d..df211e4bd0 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -// NOTE(freben): Intentionally uses node-fetch because of https://github.com/backstage/backstage/issues/28190 -import fetch from 'node-fetch'; - import { getGitLabRequestOptions, + GitLabIntegration, GitLabIntegrationConfig, } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -44,7 +42,7 @@ interface ListProjectOptions extends CommonListOptions { group?: string; membership?: boolean; topics?: string; - simple?: boolean; + last_activity_after?: string; } interface ListFilesOptions extends CommonListOptions { @@ -59,13 +57,15 @@ interface UserListOptions extends CommonListOptions { export class GitLabClient { private readonly config: GitLabIntegrationConfig; + private readonly integration: GitLabIntegration; private readonly logger: LoggerService; constructor(options: { - config: GitLabIntegrationConfig; + integration: GitLabIntegration; logger: LoggerService; }) { - this.config = options.config; + this.config = options.integration.config; + this.integration = options.integration; this.logger = options.logger; } @@ -125,6 +125,25 @@ export class GitLabClient { return response; } + async getProjectCommits( + projectId: number | string, + options?: { + since?: string; + until?: string; + ref_name?: string; + path?: string; + per_page?: number; + page?: number; + }, + ): Promise> { + const encodedProjectId = + typeof projectId === 'string' ? encodeURIComponent(projectId) : projectId; + return this.pagedRequest( + `/projects/${encodedProjectId}/repository/commits`, + options, + ); + } + async listGroupMembers( groupPath: string, options?: CommonListOptions, @@ -215,8 +234,8 @@ export class GitLabClient { let endCursor: string | null = null; do { - const response: GitLabDescendantGroupsResponse = - await this.fetchWithRetry(`${this.config.baseUrl}/api/graphql`, { + const response: GitLabDescendantGroupsResponse = await this.integration + .fetch(`${this.config.baseUrl}/api/graphql`, { method: 'POST', headers: { ...getGitLabRequestOptions(this.config).headers, @@ -247,7 +266,8 @@ export class GitLabClient { } `, }), - }).then(r => r.json()); + }) + .then(r => r.json()); if (response.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`); } @@ -289,9 +309,8 @@ export class GitLabClient { let hasNextPage: boolean = false; let endCursor: string | null = null; do { - const response: GitLabGroupMembersResponse = await this.fetchWithRetry( - `${this.config.baseUrl}/api/graphql`, - { + const response: GitLabGroupMembersResponse = await this.integration + .fetch(`${this.config.baseUrl}/api/graphql`, { method: 'POST', headers: { ...getGitLabRequestOptions(this.config).headers, @@ -331,8 +350,8 @@ export class GitLabClient { } `, }), - }, - ).then(r => r.json()); + }) + .then(r => r.json()); if (response.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`); } @@ -383,7 +402,7 @@ export class GitLabClient { const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); request.searchParams.append('ref', branch); - const response = await this.fetchWithRetry(request.toString(), { + const response = await this.integration.fetch(request.toString(), { headers: getGitLabRequestOptions(this.config).headers, method: 'HEAD', }); @@ -430,7 +449,7 @@ export class GitLabClient { } this.logger.debug(`Fetching: ${request.toString()}`); - const response = await this.fetchWithRetry( + const response = await this.integration.fetch( request.toString(), getGitLabRequestOptions(this.config), ); @@ -468,7 +487,7 @@ export class GitLabClient { } } - const response = await this.fetchWithRetry( + const response = await this.integration.fetch( request.toString(), getGitLabRequestOptions(this.config), ); @@ -483,48 +502,6 @@ export class GitLabClient { return response.json(); } - - /** - * Performs a fetch request with retry logic for rate limiting (429 errors) - * @param url - The URL to fetch - * @param options - Fetch options - * @param retries - Maximum number of retries - * @param initialBackoff - Initial backoff time in ms - */ - async fetchWithRetry( - url: string, - options: fetch.RequestInit, - retries = 5, - initialBackoff = 100, - ): Promise { - let currentRetry = 0; - let backoff = initialBackoff; - - for (;;) { - const response = await fetch(url, options); - - if (response.status !== 429 || currentRetry >= retries) { - return response; - } - - // Get retry-after header if available, or use exponential backoff - const retryAfter = response.headers.get('Retry-After'); - const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : backoff; - - this.logger.warn( - `GitLab API rate limit exceeded, retrying in ${waitTime}ms (retry ${ - currentRetry + 1 - }/${retries})`, - ); - - // Wait before retrying - await new Promise(resolve => setTimeout(resolve, waitTime)); - - // Exponential backoff with jitter - backoff = backoff * 2 * (0.8 + Math.random() * 0.4); - currentRetry++; - } - } } /** diff --git a/plugins/catalog-backend-module-gitlab/src/lib/index.ts b/plugins/catalog-backend-module-gitlab/src/lib/index.ts index cc82bd6a0b..7985d94b69 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/index.ts @@ -17,6 +17,7 @@ export { readGitlabConfigs } from '../providers/config'; export { GitLabClient, paginated } from './client'; export type { + GitLabCommit, GitLabGroup, GitLabGroupSamlIdentity, GitLabProject, diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 0d00c956e5..c4defd9fcf 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -45,6 +45,21 @@ export type GitLabProject = { forked_from_project?: GitlabProjectForkedFrom; }; +export type GitLabCommit = { + id: string; + short_id: string; + title: string; + message: string; + created_at: string; + author_name: string; + author_email: string; + authored_date: string; + committed_date: string; + committer_name: string; + committer_email: string; + web_url: string; +}; + /** * Representation of a GitLab user in the GitLab API * diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 06b3f8c158..40936a4d77 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -135,7 +135,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { this.scheduleFn = this.createScheduleFn(options.taskRunner); this.events = options.events; this.gitLabClient = new GitLabClient({ - config: this.integration.config, + integration: this.integration, logger: this.logger, }); } diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 3b8720b5f2..37b446fd95 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -204,7 +204,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { : [this.config.groupPattern]; this.gitLabClient = new GitLabClient({ - config: this.integration.config, + integration: this.integration, logger: this.logger, }); } diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index df3c81a631..1844172176 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.7.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 0.7.9 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-events-node@0.4.19 + ## 0.7.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index d929705815..8d7337796c 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.7.9-next.1", + "version": "0.7.10-next.2", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.test.ts index 486fb4b8ae..8b74e19755 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.test.ts @@ -18,7 +18,15 @@ import { IncrementalIngestionEngine } from './IncrementalIngestionEngine'; import { IterationEngineOptions } from '../types'; import { performance } from 'node:perf_hooks'; -jest.setTimeout(60_000); +jest.mock('node:perf_hooks', () => ({ + performance: { + now: jest.fn(), + }, +})); + +const mockPerformanceNow = performance.now as jest.MockedFunction< + typeof performance.now +>; describe('IncrementalIngestionEngine - Burst Length', () => { const createMockProvider = () => ({ @@ -50,6 +58,10 @@ describe('IncrementalIngestionEngine - Burst Length', () => { child: jest.fn().mockReturnThis(), } as any); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should respect burst length and stop burst when time limit exceeded', async () => { const mockProvider = createMockProvider(); const mockManager = createMockManager(); @@ -60,7 +72,7 @@ describe('IncrementalIngestionEngine - Burst Length', () => { provider: mockProvider, manager: mockManager, connection: mockConnection, - burstLength: { milliseconds: 100 }, // Short burst length for testing + burstLength: { milliseconds: 100 }, restLength: { minutes: 1 }, logger: mockLogger, ready: Promise.resolve(), @@ -69,16 +81,17 @@ describe('IncrementalIngestionEngine - Burst Length', () => { const engine = new IncrementalIngestionEngine(options); let callCount = 0; + // Simulate time advancing: start at 1000, each call advances 40ms + let currentTime = 1000; + mockPerformanceNow.mockImplementation(() => currentTime); + mockProvider.around.mockImplementation(async fn => { await fn({}); }); - // Mock provider.next to return multiple batches that never complete - // Each call takes some time to simulate real processing mockProvider.next.mockImplementation(async () => { callCount++; - // Add a small delay to ensure we exceed burst length - await new Promise(resolve => setTimeout(resolve, 30)); + currentTime += 40; return { done: false, entities: [ @@ -94,19 +107,15 @@ describe('IncrementalIngestionEngine - Burst Length', () => { }); const signal = new AbortController().signal; - const start = performance.now(); const result = await engine.ingestOneBurst('test-ingestion', signal); - const duration = performance.now() - start; - - // Verify that the burst was stopped due to time limit, not completion + // Call 1: time=1040, elapsed=40 < 100 → continue + // Call 2: time=1080, elapsed=80 < 100 → continue + // Call 3: time=1120, elapsed=120 > 100 → stop expect(result).toBe(false); - expect(duration).toBeGreaterThanOrEqual(100); - expect(duration).toBeLessThan(200); - expect(mockProvider.next).toHaveBeenCalledTimes(callCount); - - expect(callCount).toBeGreaterThan(1); + expect(mockProvider.next).toHaveBeenCalledTimes(3); + expect(callCount).toBe(3); }); it('should complete burst normally when provider returns done before burst length', async () => { @@ -127,11 +136,13 @@ describe('IncrementalIngestionEngine - Burst Length', () => { const engine = new IncrementalIngestionEngine(options); + const currentTime = 1000; + mockPerformanceNow.mockImplementation(() => currentTime); + mockProvider.around.mockImplementation(async fn => { await fn({}); }); - // Mock provider.next to return done after first call mockProvider.next.mockResolvedValueOnce({ done: true, entities: [ @@ -146,13 +157,10 @@ describe('IncrementalIngestionEngine - Burst Length', () => { }); const signal = new AbortController().signal; - const start = performance.now(); const result = await engine.ingestOneBurst('test-ingestion', signal); - const duration = performance.now() - start; expect(result).toBe(true); expect(mockProvider.next).toHaveBeenCalledTimes(1); - expect(duration).toBeLessThan(100); // Should complete quickly since provider returns done immediately }); it('should stop burst when time limit is reached', async () => { @@ -174,13 +182,17 @@ describe('IncrementalIngestionEngine - Burst Length', () => { const engine = new IncrementalIngestionEngine(options); let callCount = 0; + // Simulate time advancing: start at 1000, each call advances 30ms + let currentTime = 1000; + mockPerformanceNow.mockImplementation(() => currentTime); + mockProvider.around.mockImplementation(async fn => { await fn({}); }); mockProvider.next.mockImplementation(async () => { callCount++; - await new Promise(resolve => setTimeout(resolve, 30)); + currentTime += 30; return { done: false, entities: [ @@ -196,16 +208,14 @@ describe('IncrementalIngestionEngine - Burst Length', () => { }); const signal = new AbortController().signal; - const start = performance.now(); const result = await engine.ingestOneBurst('test-ingestion', signal); - const duration = performance.now() - start; - + // Call 1: time=1030, elapsed=30 < 80 → continue + // Call 2: time=1060, elapsed=60 < 80 → continue + // Call 3: time=1090, elapsed=90 > 80 → stop expect(result).toBe(false); expect(mockProvider.next).toHaveBeenCalledTimes(3); expect(callCount).toBe(3); - expect(duration).toBeGreaterThanOrEqual(90); - expect(duration).toBeLessThan(120); }); }); diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index b82b004184..a0810b4904 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.12.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.12.2 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.12.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 40705f0c7e..c2023991cf 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.12.2-next.1", + "version": "0.12.3-next.1", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index 07b50d2459..2e8a632165 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-backend@3.5.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.4.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.1.19-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index a669e5b952..cdde50d172 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.1.19-next.0", + "version": "0.1.20-next.1", "description": "A module that subscribes to catalog related events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 3ac45e3314..3587d0c632 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.9.1-next.1 + +### Patch Changes + +- 97eaecf: Fixed scheduler task remaining stuck in running state after pod termination by propagating AbortSignal into MicrosoftGraphOrgEntityProvider.read() +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.9.0 + +### Minor Changes + +- 8694561: Log group/user count, tenant ID, execution time as separate fields + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.9.0-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 079eb31991..abda323dcc 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.9.0-next.2", + "version": "0.9.1-next.1", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/report.api.md b/plugins/catalog-backend-module-msgraph/report.api.md index c6687a638a..7799def397 100644 --- a/plugins/catalog-backend-module-msgraph/report.api.md +++ b/plugins/catalog-backend-module-msgraph/report.api.md @@ -78,6 +78,7 @@ export class MicrosoftGraphClient { groupId: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable; // (undocumented) getGroupPhoto(groupId: string, sizeId?: string): Promise; @@ -89,13 +90,18 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', path?: string, + signal?: AbortSignal, ): AsyncIterable; getGroupUserMembers( groupId: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable; - getOrganization(tenantId: string): Promise; + getOrganization( + tenantId: string, + signal?: AbortSignal, + ): Promise; // (undocumented) getUserPhoto(userId: string, sizeId?: string): Promise; getUserPhotoWithSizeLimit( @@ -106,21 +112,25 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', path?: string, + signal?: AbortSignal, ): AsyncIterable; requestApi( path: string, query?: ODataQuery, headers?: Record, + signal?: AbortSignal, ): Promise; requestCollection( path: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable; requestRaw( url: string, headers?: Record, retryCount?: number, + signal?: AbortSignal, ): Promise; } @@ -142,7 +152,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { options: MicrosoftGraphOrgEntityProviderOptions, ): MicrosoftGraphOrgEntityProvider[]; getProviderName(): string; - read(options?: { logger?: LoggerService }): Promise; + read(options?: { + logger?: LoggerService; + signal?: AbortSignal; + }): Promise; } // @public @deprecated @@ -304,6 +317,7 @@ export function readMicrosoftGraphOrg( groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; logger: LoggerService; + signal?: AbortSignal; }, ): Promise<{ users: UserEntity[]; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 94f4e88df1..1889fc130b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -128,6 +128,7 @@ export class MicrosoftGraphClient { path: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable { // upgrade to advanced query mode transparently when "search" is used // to stay backwards compatible. @@ -151,7 +152,7 @@ export class MicrosoftGraphClient { } : {}; - let response = await this.requestApi(path, query, headers); + let response = await this.requestApi(path, query, headers, signal); for (;;) { if (response.status !== 200) { @@ -170,7 +171,12 @@ export class MicrosoftGraphClient { return; } - response = await this.requestRaw(result['@odata.nextLink'], headers); + response = await this.requestRaw( + result['@odata.nextLink'], + headers, + 2, + signal, + ); } } @@ -186,6 +192,7 @@ export class MicrosoftGraphClient { path: string, query?: ODataQuery, headers?: Record, + signal?: AbortSignal, ): Promise { const queryString = qs.stringify( { @@ -205,6 +212,8 @@ export class MicrosoftGraphClient { return await this.requestRaw( `${this.baseUrl}/${path}${queryString}`, headers, + 2, + signal, ); } @@ -218,6 +227,7 @@ export class MicrosoftGraphClient { url: string, headers?: Record, retryCount = 2, + signal?: AbortSignal, ): Promise { // Make sure that we always have a valid access token (might be cached) const urlObj = new URL(url); @@ -235,10 +245,11 @@ export class MicrosoftGraphClient { ...headers, Authorization: `Bearer ${token.token}`, }, + signal, }); } catch (e: any) { if (e?.code === 'ETIMEDOUT' && retryCount > 0) { - return this.requestRaw(url, headers, retryCount - 1); + return this.requestRaw(url, headers, retryCount - 1, signal); } throw e; } @@ -280,8 +291,14 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', path: string = 'users', + signal?: AbortSignal, ): AsyncIterable { - yield* this.requestCollection(path, query, queryMode); + yield* this.requestCollection( + path, + query, + queryMode, + signal, + ); } /** @@ -320,8 +337,14 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', path: string = 'groups', + signal?: AbortSignal, ): AsyncIterable { - yield* this.requestCollection(path, query, queryMode); + yield* this.requestCollection( + path, + query, + queryMode, + signal, + ); } /** @@ -336,11 +359,13 @@ export class MicrosoftGraphClient { groupId: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable { yield* this.requestCollection( `groups/${groupId}/members`, query, queryMode, + signal, ); } @@ -357,11 +382,13 @@ export class MicrosoftGraphClient { groupId: string, query?: ODataQuery, queryMode?: 'basic' | 'advanced', + signal?: AbortSignal, ): AsyncIterable { yield* this.requestCollection( `groups/${groupId}/members/microsoft.graph.user/`, query, queryMode, + signal, ); } @@ -374,8 +401,14 @@ export class MicrosoftGraphClient { */ async getOrganization( tenantId: string, + signal?: AbortSignal, ): Promise { - const response = await this.requestApi(`organization/${tenantId}`); + const response = await this.requestApi( + `organization/${tenantId}`, + undefined, + undefined, + signal, + ); if (response.status !== 200) { await this.handleError(`organization/${tenantId}`, response); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index bd58881728..d4fe3783db 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -140,6 +140,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( @@ -188,6 +189,7 @@ describe('read microsoft graph', () => { }, 'advanced', undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( @@ -232,6 +234,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( @@ -280,6 +283,7 @@ describe('read microsoft graph', () => { }, undefined, '/users/x/y', + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( @@ -332,6 +336,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); @@ -339,6 +344,7 @@ describe('read microsoft graph', () => { 'groupid', { top: 999 }, undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); @@ -391,6 +397,7 @@ describe('read microsoft graph', () => { }, 'advanced', undefined, + undefined, ); expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); @@ -398,6 +405,7 @@ describe('read microsoft graph', () => { 'groupid', { top: 999 }, 'advanced', + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); @@ -447,6 +455,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); @@ -457,6 +466,7 @@ describe('read microsoft graph', () => { top: 999, }, undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); @@ -509,6 +519,7 @@ describe('read microsoft graph', () => { }, undefined, '/groups/x/y', + undefined, ); }); }); @@ -545,7 +556,10 @@ describe('read microsoft graph', () => { ); expect(client.getOrganization).toHaveBeenCalledTimes(1); - expect(client.getOrganization).toHaveBeenCalledWith('tenantid'); + expect(client.getOrganization).toHaveBeenCalledWith( + 'tenantid', + undefined, + ); }); it('should read organization with custom transformer', async () => { @@ -563,7 +577,10 @@ describe('read microsoft graph', () => { expect(rootGroup).toEqual(undefined); expect(client.getOrganization).toHaveBeenCalledTimes(1); - expect(client.getOrganization).toHaveBeenCalledWith('tenantid'); + expect(client.getOrganization).toHaveBeenCalledWith( + 'tenantid', + undefined, + ); }); }); @@ -636,11 +653,17 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -716,11 +739,17 @@ describe('read microsoft graph', () => { }, 'advanced', undefined, + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -797,11 +826,17 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -871,11 +906,17 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); }); it('should read groups and their sub groups', async () => { @@ -976,14 +1017,25 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(2); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); - expect(client.getGroupMembers).toHaveBeenCalledWith('childgroupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'childgroupid', + { + top: 999, + }, + undefined, + undefined, + ); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -1056,11 +1108,17 @@ describe('read microsoft graph', () => { }, undefined, '/groups/x/y', + undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { - top: 999, - }); + expect(client.getGroupMembers).toHaveBeenCalledWith( + 'groupid', + { + top: 999, + }, + undefined, + undefined, + ); }); }); @@ -1211,6 +1269,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroups).toHaveBeenCalledTimes(1); expect(client.getGroups).toHaveBeenCalledWith( @@ -1220,6 +1279,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); }); @@ -1253,6 +1313,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroups).toHaveBeenCalledTimes(1); expect(client.getGroups).toHaveBeenCalledWith( @@ -1262,6 +1323,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); }); @@ -1293,6 +1355,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroups).toHaveBeenCalledTimes(1); expect(client.getGroups).toHaveBeenCalledWith( @@ -1301,6 +1364,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); }); @@ -1331,6 +1395,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); }); @@ -1369,6 +1434,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroups).toHaveBeenNthCalledWith( 2, @@ -1378,6 +1444,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); }); @@ -1419,6 +1486,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); expect(client.getGroups).toHaveBeenCalledTimes(1); expect(client.getGroups).toHaveBeenCalledWith( @@ -1427,6 +1495,7 @@ describe('read microsoft graph', () => { }, undefined, undefined, + undefined, ); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 7202f70384..4a6c4163d1 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -53,6 +53,7 @@ export async function readMicrosoftGraphUsers( loadUserPhotos?: boolean; transformer?: UserTransformer; logger: LoggerService; + signal?: AbortSignal; }, ): Promise<{ users: UserEntity[]; // With all relations empty @@ -66,6 +67,7 @@ export async function readMicrosoftGraphUsers( }, options.queryMode, options.userPath, + options.signal, ); return { @@ -93,6 +95,7 @@ export async function readMicrosoftGraphUsersInGroups( groupExpand?: string; transformer?: UserTransformer; logger: LoggerService; + signal?: AbortSignal; }, ): Promise<{ users: UserEntity[]; // With all relations empty @@ -112,6 +115,7 @@ export async function readMicrosoftGraphUsersInGroups( }, options.queryMode, options.userGroupMemberPath, + options.signal, )) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( @@ -126,6 +130,7 @@ export async function readMicrosoftGraphUsersInGroups( top: PAGE_SIZE, }, options.queryMode, + options.signal, )) { userGroupMembers.set(user.id!, user); groupMemberCount++; @@ -161,12 +166,12 @@ export async function readMicrosoftGraphUsersInGroups( export async function readMicrosoftGraphOrganization( client: MicrosoftGraphClient, tenantId: string, - options?: { transformer?: OrganizationTransformer }, + options?: { transformer?: OrganizationTransformer; signal?: AbortSignal }, ): Promise<{ rootGroup?: GroupEntity; // With all relations empty }> { // For now we expect a single root organization - const organization = await client.getOrganization(tenantId); + const organization = await client.getOrganization(tenantId, options?.signal); const transformer = options?.transformer ?? defaultOrganizationTransformer; const rootGroup = await transformer(organization); @@ -186,6 +191,7 @@ export async function readMicrosoftGraphGroups( groupIncludeSubGroups?: boolean; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; + signal?: AbortSignal; }, ): Promise<{ groups: GroupEntity[]; // With all relations empty @@ -200,6 +206,7 @@ export async function readMicrosoftGraphGroups( const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId, { transformer: options?.organizationTransformer, + signal: options?.signal, }); if (rootGroup) { groupMember.set(rootGroup.metadata.name, new Set()); @@ -219,6 +226,7 @@ export async function readMicrosoftGraphGroups( }, options?.queryMode, options?.groupPath, + options?.signal, )) { // Process all groups in parallel, otherwise it can take quite some time promises.push( @@ -238,9 +246,14 @@ export async function readMicrosoftGraphGroups( return; } - for await (const member of client.getGroupMembers(group.id!, { - top: PAGE_SIZE, - })) { + for await (const member of client.getGroupMembers( + group.id!, + { + top: PAGE_SIZE, + }, + undefined, + options?.signal, + )) { if (!member.id) { continue; } @@ -261,6 +274,8 @@ export async function readMicrosoftGraphGroups( for await (const subMember of client.getGroupMembers( member.id!, { top: PAGE_SIZE }, + undefined, + options?.signal, )) { if (!subMember.id) { continue; @@ -425,6 +440,7 @@ export async function readMicrosoftGraphOrg( groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; logger: LoggerService; + signal?: AbortSignal; }, ): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { let users: UserEntity[] = []; @@ -447,6 +463,7 @@ export async function readMicrosoftGraphOrg( loadUserPhotos: options.loadUserPhotos, transformer: options.userTransformer, logger: options.logger, + signal: options.signal, }, ); users = usersInGroups; @@ -460,6 +477,7 @@ export async function readMicrosoftGraphOrg( loadUserPhotos: options.loadUserPhotos, transformer: options.userTransformer, logger: options.logger, + signal: options.signal, }); users = usersWithFilter; } @@ -474,6 +492,7 @@ export async function readMicrosoftGraphOrg( groupIncludeSubGroups: options.groupIncludeSubGroups, groupTransformer: options.groupTransformer, organizationTransformer: options.organizationTransformer, + signal: options.signal, }); resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts index 1d68f706bc..786790bb2f 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts @@ -255,6 +255,57 @@ describe('MicrosoftGraphOrgEntityProvider', () => { ); }); + it('should stop processing when AbortSignal is aborted', async () => { + jest.spyOn(logger, 'child').mockReturnValue(logger as any); + + const config = new ConfigReader({ + catalog: { + providers: { + microsoftGraphOrg: { + customProviderId: { + target: 'target', + tenantId: 'tenantId', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }, + }, + }, + }); + const localTaskRunner = new PersistingTaskRunner(); + const provider = MicrosoftGraphOrgEntityProvider.fromConfig(config, { + logger, + schedule: localTaskRunner, + })[0]; + + await provider.connect(entityProviderConnection); + + const controller = new AbortController(); + controller.abort(); + + readMicrosoftGraphOrgMocked.mockImplementationOnce( + async (_client, _tenantId, options) => { + if (options.signal?.aborted) { + const error = new Error('The operation was aborted'); + error.name = 'AbortError'; + throw error; + } + return { users: [], groups: [] }; + }, + ); + + const taskDef = localTaskRunner.getTasks()[0]; + // Should resolve without throwing (abort is caught and logged at debug level) + await (taskDef.fn as (signal: AbortSignal) => Promise)( + controller.signal, + ); + + expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('refresh aborted due to shutdown'), + ); + }); + it('fail without schedule and scheduler', () => { const config = new ConfigReader({ catalog: { diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index b223c18d6e..a6f2ee78d7 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -326,7 +326,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { * Runs one complete ingestion loop. Call this method regularly at some * appropriate cadence. */ - async read(options?: { logger?: LoggerService }) { + async read(options?: { logger?: LoggerService; signal?: AbortSignal }) { if (!this.connection) { throw new Error('Not initialized'); } @@ -361,6 +361,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer: this.options.userTransformer, organizationTransformer: this.options.organizationTransformer, logger: logger, + signal: options?.signal, }, ); @@ -386,7 +387,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { const id = `${this.getProviderName()}:refresh`; await taskRunner.run({ id, - fn: async () => { + fn: async (abortSignal: AbortSignal) => { const logger = this.options.logger.child({ class: MicrosoftGraphOrgEntityProvider.prototype.constructor.name, taskId: id, @@ -394,8 +395,14 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { }); try { - await this.read({ logger }); + await this.read({ logger, signal: abortSignal }); } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + logger.debug( + `${this.getProviderName()} refresh aborted due to shutdown`, + ); + return; + } logger.error( `${this.getProviderName()} refresh failed, ${error}`, error, diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 6ca8223cde..46b753fab6 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.2.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.2.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.2.19-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index b2cb5d4cc3..1c6eba835d 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.2.19-next.2", + "version": "0.2.20-next.2", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index c975166273..4d721d9cc0 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.2.19 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + ## 0.2.19-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index d93972c9e9..377273d261 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.19-next.1", + "version": "0.2.20-next.1", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 4f41840d2f..f8267e01fc 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## 0.2.17 + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 0.2.17-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index e75aa5aff8..446ca9bb16 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.2.17-next.1", + "version": "0.2.18-next.2", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index f84459dfaa..db62abafad 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.6.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.6.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-permission-common@0.9.6 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + ## 0.6.8-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index 81266ce5eb..62a7b18a54 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.6.8-next.1", + "version": "0.6.9-next.1", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index dd6c872ceb..3fab6809ae 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,145 @@ # @backstage/plugin-catalog-backend +## 3.5.0-next.2 + +### Minor Changes + +- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error. + +### Patch Changes + +- 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 3.5.0-next.1 + +### Minor Changes + +- a6b2819: Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 972f686: Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- 72747b4: Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules: + + - `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead + - `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 3.5.0-next.0 + +### Minor Changes + +- bf71677: Added opentelemetry metrics for SCM events: + + - `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`. + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- fbf382f: Minor internal optimisation +- 1ee5b28: Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes. +- 3181973: Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state` +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 3.4.0 + +### Minor Changes + +- f1d29b4: Failures to connect catalog providers are now attributed to the module that provided the failing provider. This means that such failures will be reported as module startup failures rather than a failure to start the catalog plugin, and will therefore respect `onPluginModuleBootFailure` configuration instead. +- 34cc520: Implemented handling of events from the newly introduced alpha + `catalogScmEventsServiceRef` service, in the builtin entity providers. This + allows entities to get refreshed, and locations updated or removed, as a + response to incoming events. In its first iteration, only the GitHub module + implements such event handling however. + + This is not yet enabled by default, but this fact may change in a future + release. To try it out, ensure that you have the latest catalog GitHub module + installed, and set the following in your app-config: + + ```yaml + catalog: + scmEvents: true + ``` + + Or if you want to pick and choose from the various features: + + ```yaml + catalog: + scmEvents: + # refresh (reprocess) upon events? + refresh: true + # automatically unregister locations based on events? (files deleted, repos archived, etc) + unregister: true + # automatically move locations based on events? (repo transferred, file renamed, etc) + move: true + ``` + +- b4e8249: Implemented the `POST /locations/by-query` endpoint which allows paginated, filtered location queries + +### Patch Changes + +- cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports. +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 08a5813: Fixed O(n²) performance bottleneck in `buildEntitySearch` `traverse()` by replacing `Array.some()` linear scan with a `Set` for O(1) duplicate path key detection. +- 1e669cc: Migrate audit events reference docs to http://backstage.io/docs. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.19 + ## 3.4.0-next.2 ### Patch Changes diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 48dae2d747..433d8c494c 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -191,6 +191,15 @@ export interface Config { stitchTimeout?: HumanDuration | string; }; + /** + * The strategy to use when there is a conflict with a location being registered. + * + * The default value is "reject". + * + * The "refresh" strategy will refresh the existing location instead of throwing a conflict error. + */ + defaultLocationConflictStrategy?: 'refresh' | 'reject'; + /** * The interval at which the catalog should process its entities. * @remarks diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js new file mode 100644 index 0000000000..4d43baf9a8 --- /dev/null +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -0,0 +1,253 @@ +/* + * Copyright 2026 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. + */ + +// @ts-check + +const BATCH_SIZE = 10000; + +/** + * Batch-deletes orphaned search rows whose entity_id doesn't exist in the + * given reference table. Processes in chunks to avoid long locks. + * + * @param {import('knex').Knex} knex + * @param {string} refTable - The table to check entity_id against + */ +async function batchDeleteOrphansPg(knex, refTable) { + for (;;) { + const deleted = await knex.raw(` + DELETE FROM "search" + WHERE ctid IN ( + SELECT s.ctid FROM "search" s + LEFT JOIN "${refTable}" r ON s."entity_id" = r."entity_id" + WHERE r."entity_id" IS NULL + AND s."entity_id" IS NOT NULL + LIMIT ${BATCH_SIZE} + ) + `); + if (deleted.rowCount === 0) { + break; + } + } +} + +/** + * @param {import('knex').Knex} knex + * @param {string} refTable + */ +async function batchDeleteOrphansMysql(knex, refTable) { + for (;;) { + const [orphanIds] = await knex.raw(` + SELECT DISTINCT s.\`entity_id\` FROM \`search\` s + LEFT JOIN \`${refTable}\` r ON s.\`entity_id\` = r.\`entity_id\` + WHERE r.\`entity_id\` IS NULL + AND s.\`entity_id\` IS NOT NULL + LIMIT ${BATCH_SIZE} + `); + if (orphanIds.length === 0) { + break; + } + const ids = orphanIds.map( + (/** @type {{ entity_id: string }} */ r) => r.entity_id, + ); + await knex('search').whereIn('entity_id', ids).delete(); + } +} + +/** + * Changes the search table's foreign key from refresh_state(entity_id) + * to final_entities(entity_id). This allows search entries to reference + * final entities directly, with CASCADE delete when entities are removed. + * + * On PostgreSQL, the migration first switches the foreign key to point at + * final_entities using a single ALTER TABLE with a NOT VALID constraint, + * then batch-deletes any pre-existing orphaned rows outside of DDL, and + * finally VALIDATEs the constraint to keep the AccessExclusiveLock window + * as short as possible. + * + * On MySQL, the migration batch-deletes orphaned rows in chunks around the + * foreign key change to reduce lock time on large tables. + * + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + const client = knex.client.config.client; + + if (client.includes('pg')) { + // Drop old FK and immediately add the new one as NOT VALID in a single + // ALTER TABLE statement. This prevents new orphan rows from being + // inserted while we clean up existing ones, and eliminates any window + // where no FK exists at all. + await knex.raw(` + ALTER TABLE "search" + DROP CONSTRAINT IF EXISTS "search_entity_id_foreign", + ADD CONSTRAINT "search_entity_id_foreign" + FOREIGN KEY ("entity_id") REFERENCES "final_entities"("entity_id") + ON DELETE CASCADE + NOT VALID + `); + + // Batch-delete orphaned rows that existed before the NOT VALID FK was + // added. This runs outside any DDL lock, so it doesn't block reads. + await batchDeleteOrphansPg(knex, 'final_entities'); + + // Validate the FK separately. This only takes a + // ShareUpdateExclusiveLock, which does not block normal reads/writes + // (DML) but can still conflict with some DDL or maintenance operations. + await knex.raw( + `ALTER TABLE "search" VALIDATE CONSTRAINT "search_entity_id_foreign"`, + ); + } else if (client.includes('mysql')) { + // Batch-delete orphaned rows before DDL to reduce lock time. + await batchDeleteOrphansMysql(knex, 'final_entities'); + + // Swap the FK with retry logic. MySQL DDL causes implicit commits so + // DROP and ADD are never truly atomic. If new orphan rows sneak in + // between the DROP and ADD, the ADD will fail — we clean up and retry. + // The information_schema check makes the DROP idempotent so that + // re-runs after a partial failure don't crash. + const MAX_ATTEMPTS = 5; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const [fks] = await knex.raw(` + SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'search' + AND CONSTRAINT_NAME = 'search_entity_id_foreign' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + `); + if (fks.length > 0) { + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + } + + await batchDeleteOrphansMysql(knex, 'final_entities'); + + try { + await knex.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); + break; + } catch (e) { + if (attempt === MAX_ATTEMPTS) throw e; + } + } + } else { + // SQLite: wrap in an explicit transaction since the global transaction + // wrapper is disabled for this migration. + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + + await trx('search') + .whereNotIn('entity_id', trx('final_entities').select('entity_id')) + .delete(); + + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); + }); + } +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + const client = knex.client.config.client; + + if (client.includes('pg')) { + await knex.raw(` + ALTER TABLE "search" + DROP CONSTRAINT IF EXISTS "search_entity_id_foreign", + ADD CONSTRAINT "search_entity_id_foreign" + FOREIGN KEY ("entity_id") REFERENCES "refresh_state"("entity_id") + ON DELETE CASCADE + NOT VALID + `); + + await batchDeleteOrphansPg(knex, 'refresh_state'); + + await knex.raw( + `ALTER TABLE "search" VALIDATE CONSTRAINT "search_entity_id_foreign"`, + ); + } else if (client.includes('mysql')) { + await batchDeleteOrphansMysql(knex, 'refresh_state'); + + const MAX_ATTEMPTS = 5; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const [fks] = await knex.raw(` + SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'search' + AND CONSTRAINT_NAME = 'search_entity_id_foreign' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + `); + if (fks.length > 0) { + await knex.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + } + + await batchDeleteOrphansMysql(knex, 'refresh_state'); + + try { + await knex.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); + break; + } catch (e) { + if (attempt === MAX_ATTEMPTS) throw e; + } + } + } else { + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + + await trx('search') + .whereNotIn('entity_id', trx('refresh_state').select('entity_id')) + .delete(); + + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); + }); + } +}; + +// Disable the default transaction wrapper so the batched deletes run +// outside of the DDL transaction that holds AccessExclusiveLock. +exports.config = { + transaction: false, +}; diff --git a/plugins/catalog-backend/migrations/20260215000000_move_stitch_queue.js b/plugins/catalog-backend/migrations/20260215000000_move_stitch_queue.js new file mode 100644 index 0000000000..7dd11db78b --- /dev/null +++ b/plugins/catalog-backend/migrations/20260215000000_move_stitch_queue.js @@ -0,0 +1,174 @@ +/* + * Copyright 2026 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. + */ + +// @ts-check + +/** + * Creates a dedicated stitch_queue table and moves the stitch queue columns + * from refresh_state into it. The stitch queue semantically is a separate + * concern from the refresh state. + * + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + const isSQLite = knex.client.config.client.includes('sqlite'); + + // Step 1: Create the stitch_queue table + await knex.schema.createTable('stitch_queue', table => { + table + .string('entity_ref', 255) + .primary() + .notNullable() + .comment('The entity ref that needs stitching'); + table + .string('stitch_ticket') + .notNullable() + .comment('Changes with every new stitch request'); + table + .dateTime('next_stitch_at') + .notNullable() + .index('stitch_queue_next_stitch_at_idx') + .comment('Timestamp of when entity should be stitched'); + }); + + // Step 2: Migrate existing stitch requests from refresh_state to stitch_queue + const pendingStitches = await knex + .select({ + entity_ref: 'refresh_state.entity_ref', + next_stitch_at: 'refresh_state.next_stitch_at', + next_stitch_ticket: 'refresh_state.next_stitch_ticket', + }) + .from('refresh_state') + .whereNotNull('refresh_state.next_stitch_at'); + + await knex.batchInsert( + 'stitch_queue', + pendingStitches.map(row => ({ + entity_ref: row.entity_ref, + stitch_ticket: row.next_stitch_ticket || '', + next_stitch_at: row.next_stitch_at, + })), + 1000, + ); + + // Step 3: Remove next_stitch_at and next_stitch_ticket columns from refresh_state + if (isSQLite) { + await knex.raw('DROP INDEX IF EXISTS refresh_state_next_stitch_at_idx'); + await knex.raw('ALTER TABLE refresh_state DROP COLUMN next_stitch_at'); + await knex.raw('ALTER TABLE refresh_state DROP COLUMN next_stitch_ticket'); + } else { + await knex.schema.alterTable('refresh_state', table => { + table.dropIndex([], 'refresh_state_next_stitch_at_idx'); + table.dropColumn('next_stitch_at'); + table.dropColumn('next_stitch_ticket'); + }); + } + + // Step 4: Remove stitch_ticket from final_entities (now managed via stitch_queue) + if (isSQLite) { + await knex.raw('ALTER TABLE final_entities DROP COLUMN stitch_ticket'); + } else { + await knex.schema.alterTable('final_entities', table => { + table.dropColumn('stitch_ticket'); + }); + } +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + const isSQLite = knex.client.config.client.includes('sqlite'); + + // Step 1: Add back stitch_ticket to final_entities + await knex.schema.alterTable('final_entities', table => { + table + .text('stitch_ticket') + .nullable() + .comment('Random value representing a unique stitch attempt ticket'); + }); + await knex('final_entities').update({ stitch_ticket: '' }); + if (isSQLite) { + // SQLite doesn't support ALTER COLUMN, but the nullable column is fine + } else { + await knex.schema.alterTable('final_entities', table => { + table.text('stitch_ticket').notNullable().alter(); + }); + } + + // Step 2: Add back the columns to refresh_state + await knex.schema.alterTable('refresh_state', table => { + table + .dateTime('next_stitch_at') + .nullable() + .comment('Timestamp of when entity should be stitched'); + table + .string('next_stitch_ticket') + .nullable() + .comment('Random value distinguishing stitch requests'); + }); + + // Step 2b: Create partial index separately + if (isSQLite) { + await knex.raw(` + CREATE INDEX refresh_state_next_stitch_at_idx + ON refresh_state (next_stitch_at) + WHERE next_stitch_at IS NOT NULL + `); + } else { + await knex.schema.alterTable('refresh_state', table => { + table.index('next_stitch_at', 'refresh_state_next_stitch_at_idx', { + predicate: knex.whereNotNull('next_stitch_at'), + }); + }); + } + + // Step 3: Migrate stitch requests back from stitch_queue to refresh_state + if (isSQLite) { + const pendingStitches = await knex + .select({ + entityRef: 'stitch_queue.entity_ref', + nextStitchAt: 'stitch_queue.next_stitch_at', + latestTicket: 'stitch_queue.stitch_ticket', + }) + .from('stitch_queue'); + + for (const row of pendingStitches) { + await knex('refresh_state') + .update({ + next_stitch_at: row.nextStitchAt, + next_stitch_ticket: row.latestTicket, + }) + .where('entity_ref', row.entityRef); + } + } else { + await knex('refresh_state') + .update({ + next_stitch_at: knex + .select('stitch_queue.next_stitch_at') + .from('stitch_queue') + .whereRaw('stitch_queue.entity_ref = refresh_state.entity_ref'), + next_stitch_ticket: knex + .select('stitch_queue.stitch_ticket') + .from('stitch_queue') + .whereRaw('stitch_queue.entity_ref = refresh_state.entity_ref'), + }) + .whereIn('entity_ref', knex.select('entity_ref').from('stitch_queue')); + } + + // Step 4: Drop the stitch_queue table + await knex.schema.dropTable('stitch_queue'); +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c537ffae84..5f034ed3e5 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.4.0-next.2", + "version": "3.5.0-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", @@ -89,7 +89,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", - "minimatch": "^9.0.0", + "minimatch": "^10.2.1", "p-limit": "^3.0.2", "prom-client": "^15.0.0", "uuid": "^11.0.0", diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index 9fc3041d6f..610548aec6 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -31,7 +31,7 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { constructor(opts: { scmIntegrationRegistry: ScmIntegrationRegistry; @@ -76,7 +76,7 @@ export const CATALOG_ERRORS_TOPIC = 'experimental.catalog.errors'; const catalogPlugin: BackendFeature; export default catalogPlugin; -// @public (undocumented) +// @public @deprecated (undocumented) export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/catalog-backend/report.sql.md b/plugins/catalog-backend/report.sql.md index 4f76d2a3f5..dd5892b7fb 100644 --- a/plugins/catalog-backend/report.sql.md +++ b/plugins/catalog-backend/report.sql.md @@ -19,7 +19,6 @@ | `final_entity` | `text` | true | - | - | | `hash` | `character varying` | false | 255 | - | | `last_updated_at` | `timestamp with time zone` | true | - | - | -| `stitch_ticket` | `text` | false | - | - | ### Indices @@ -76,8 +75,6 @@ | `errors` | `text` | false | - | - | | `last_discovery_at` | `timestamp with time zone` | false | - | - | | `location_key` | `text` | true | - | - | -| `next_stitch_at` | `timestamp with time zone` | true | - | - | -| `next_stitch_ticket` | `character varying` | true | 255 | - | | `next_update_at` | `timestamp with time zone` | false | - | - | | `processed_entity` | `text` | true | - | - | | `result_hash` | `text` | true | - | - | @@ -87,7 +84,6 @@ ### Indices - `refresh_state_entity_ref_uniq` (`entity_ref`) unique -- `refresh_state_next_stitch_at_idx` (`next_stitch_at`) - `refresh_state_next_update_at_idx` (`next_update_at`) - `refresh_state_pkey` (`entity_id`) unique primary @@ -135,3 +131,16 @@ - `search_entity_id_idx` (`entity_id`) - `search_key_original_value_idx` (`key`, `original_value`) - `search_key_value_idx` (`key`, `value`) + +## Table `stitch_queue` + +| Column | Type | Nullable | Max Length | Default | +| ---------------- | -------------------------- | -------- | ---------- | ------- | +| `entity_ref` | `character varying` | false | 255 | - | +| `next_stitch_at` | `timestamp with time zone` | false | - | - | +| `stitch_ticket` | `character varying` | false | 255 | - | + +### Indices + +- `stitch_queue_next_stitch_at_idx` (`next_stitch_at`) +- `stitch_queue_pkey` (`entity_ref`) unique primary diff --git a/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.test.ts b/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.test.ts new file mode 100644 index 0000000000..3be032a161 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.test.ts @@ -0,0 +1,344 @@ +/* + * 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 { createQueryCatalogEntitiesAction } from './createQueryCatalogEntitiesAction'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; + +const testEntities = [ + { + kind: 'Component', + apiVersion: 'backstage.io/v1alpha1', + metadata: { + name: 'service-a', + namespace: 'default', + annotations: { 'backstage.io/techdocs-ref': 'dir:.' }, + }, + spec: { + type: 'service', + dependsOn: ['component:default/shared-lib', 'api:default/user-api'], + }, + }, + { + kind: 'Component', + apiVersion: 'backstage.io/v1alpha1', + metadata: { name: 'website-b', namespace: 'default' }, + spec: { type: 'website' }, + }, + { + kind: 'API', + apiVersion: 'backstage.io/v1alpha1', + metadata: { name: 'user-api', namespace: 'default' }, + spec: { type: 'openapi' }, + }, + { + kind: 'Group', + apiVersion: 'backstage.io/v1alpha1', + metadata: { name: 'team-alpha', namespace: 'default' }, + }, +]; + +function createCatalogQueryAction(options?: { + entities?: typeof testEntities; +}) { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock({ + entities: options?.entities ?? testEntities, + }); + createQueryCatalogEntitiesAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + return { invoke: mockActionsRegistry.invoke.bind(mockActionsRegistry) }; +} + +describe('createQueryCatalogEntitiesAction', () => { + it('should return all entities when no filter is provided', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: {}, + }); + + expect(result.output).toEqual({ + items: testEntities, + totalItems: 4, + hasMoreEntities: false, + nextPageCursor: undefined, + }); + }); + + it('should return empty results when no entities match', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { query: { kind: 'NonExistent' } }, + }); + + expect(result.output).toEqual({ + items: [], + totalItems: 0, + hasMoreEntities: false, + nextPageCursor: undefined, + }); + }); + + it('should filter by kind', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { query: { kind: 'Component' } }, + }); + + expect(result.output).toMatchObject({ + totalItems: 2, + items: expect.arrayContaining([ + expect.objectContaining({ + metadata: { + name: 'service-a', + namespace: 'default', + annotations: { 'backstage.io/techdocs-ref': 'dir:.' }, + }, + }), + expect.objectContaining({ + metadata: { name: 'website-b', namespace: 'default' }, + }), + ]), + }); + }); + + it('should filter with multiple conditions', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { kind: 'Component', 'spec.type': 'service' }, + }, + }); + + expect(result.output).toMatchObject({ + totalItems: 1, + items: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'service-a' }), + }), + ], + }); + }); + + it('should support $not operator', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { query: { $not: { kind: 'Group' } } }, + }); + + expect(result.output).toMatchObject({ totalItems: 3 }); + const items = (result.output as any).items; + expect(items.every((e: any) => e.kind !== 'Group')).toBe(true); + }); + + it('should support $all operator', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { + $all: [{ kind: 'Component' }, { 'spec.type': 'service' }], + }, + }, + }); + + expect(result.output).toMatchObject({ + totalItems: 1, + items: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'service-a' }), + }), + ], + }); + }); + + it('should support $any operator', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { + $any: [{ kind: 'API' }, { kind: 'Group' }], + }, + }, + }); + + expect(result.output).toMatchObject({ totalItems: 2 }); + const items = (result.output as any).items; + expect( + items.every((e: any) => e.kind === 'API' || e.kind === 'Group'), + ).toBe(true); + }); + + it('should support $exists: true', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { + 'metadata.annotations.backstage.io/techdocs-ref': { $exists: true }, + }, + }, + }); + + expect(result.output).toMatchObject({ + totalItems: 1, + items: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'service-a' }), + }), + ], + }); + }); + + it('should support $exists: false', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { + 'metadata.annotations.backstage.io/techdocs-ref': { $exists: false }, + }, + }, + }); + + expect(result.output).toMatchObject({ totalItems: 3 }); + const items = (result.output as any).items; + expect(items.every((e: any) => e.metadata.name !== 'service-a')).toBe(true); + }); + + it('should support $in operator', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { 'spec.type': { $in: ['service', 'openapi'] } }, + }, + }); + + expect(result.output).toMatchObject({ totalItems: 2 }); + const names = (result.output as any).items.map((e: any) => e.metadata.name); + expect(names).toEqual(expect.arrayContaining(['service-a', 'user-api'])); + }); + + it('should support $contains operator', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { + 'spec.dependsOn': { $contains: 'component:default/shared-lib' }, + }, + }, + }); + + expect(result.output).toMatchObject({ + totalItems: 1, + items: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'service-a' }), + }), + ], + }); + }); + + it('should support pagination with limit', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { limit: 2 }, + }); + + const output = result.output as any; + expect(output.items).toHaveLength(2); + expect(output.totalItems).toBe(4); + expect(output.hasMoreEntities).toBe(true); + expect(output.nextPageCursor).toBeDefined(); + }); + + it('should support cursor-based pagination', async () => { + const { invoke } = createCatalogQueryAction(); + + const firstPage = await invoke({ + id: 'test:query-catalog-entities', + input: { limit: 2 }, + }); + const firstOutput = firstPage.output as any; + + const secondPage = await invoke({ + id: 'test:query-catalog-entities', + input: { cursor: firstOutput.nextPageCursor, limit: 2 }, + }); + const secondOutput = secondPage.output as any; + + expect(secondOutput.items).toHaveLength(2); + expect(secondOutput.hasMoreEntities).toBe(false); + }); + + it('should support orderFields', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { kind: 'Component' }, + orderFields: { field: 'metadata.name', order: 'desc' }, + }, + }); + + const names = (result.output as any).items.map((e: any) => e.metadata.name); + expect(names).toEqual(['website-b', 'service-a']); + }); + + it('should support array orderFields for multi-field sorting', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + orderFields: [ + { field: 'kind', order: 'asc' }, + { field: 'metadata.name', order: 'asc' }, + ], + }, + }); + + const kinds = (result.output as any).items.map((e: any) => e.kind); + expect(kinds).toEqual(['API', 'Component', 'Component', 'Group']); + }); + + it('should support fields projection', async () => { + const { invoke } = createCatalogQueryAction(); + const result = await invoke({ + id: 'test:query-catalog-entities', + input: { + query: { kind: 'Component', 'spec.type': 'service' }, + fields: ['kind', 'metadata.name'], + }, + }); + + const items = (result.output as any).items; + expect(items).toHaveLength(1); + expect(items[0]).toEqual({ + kind: 'Component', + metadata: { name: 'service-a' }, + }); + }); +}); diff --git a/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.ts b/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.ts new file mode 100644 index 0000000000..6bf4171163 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createQueryCatalogEntitiesAction.ts @@ -0,0 +1,220 @@ +/* + * 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates'; + +export const createQueryCatalogEntitiesAction = ({ + catalog, + actionsRegistry, +}: { + catalog: CatalogService; + actionsRegistry: ActionsRegistryService; +}) => { + actionsRegistry.register({ + name: 'query-catalog-entities', + title: 'Query Catalog Entities', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: ` +Query entities from the Backstage Software Catalog using predicate filters. + +## Catalog Model + +The catalog contains entities of different kinds. Every entity has "kind", "apiVersion", "metadata", and optionally "spec" and "relations". Fields use dot notation for querying. + +Common metadata fields on all entities: name, namespace (default: "default"), title, description, labels, annotations, tags (string array), links. + +Entity references use the format "kind:namespace/name", e.g. "component:default/my-service" or "user:default/jane.doe". + +### Entity Kinds + +**Component** - A piece of software such as a service, website, or library. + spec fields: type (e.g. "service", "website", "library"), lifecycle (e.g. "production", "experimental", "deprecated"), owner (entity ref), system, subcomponentOf, providesApis, consumesApis, dependsOn, dependencyOf. + +**API** - An interface that components expose, such as REST APIs or event streams. + spec fields: type (e.g. "openapi", "asyncapi", "graphql", "grpc"), lifecycle, owner (entity ref), definition (the API spec content), system. + +**System** - A collection of components, APIs, and resources that together expose some functionality. + spec fields: owner (entity ref), domain, type. + +**Domain** - A grouping of systems that share terminology, domain models, and business purpose. + spec fields: owner (entity ref), subdomainOf, type. + +**Resource** - Infrastructure required to operate a component, such as databases or storage buckets. + spec fields: type, owner (entity ref), system, dependsOn, dependencyOf. + +**Group** - An organizational entity such as a team or business unit. + spec fields: type (e.g. "team", "business-unit"), children (entity refs), parent (entity ref), members (entity refs), profile (displayName, email, picture). + +**User** - A person, such as an employee or contractor. + spec fields: memberOf (entity refs), profile (displayName, email, picture). + +**Location** - A marker that references other catalog descriptor files to be ingested. + spec fields: type, target, targets, presence. + +### Relations + +Entities have bidirectional relations stored in the "relations" array. Common relation types: ownedBy/ownerOf, dependsOn/dependencyOf, providesApi/apiProvidedBy, consumesApi/apiConsumedBy, parentOf/childOf, memberOf/hasMember, partOf/hasPart. + +Relations can be queried via "relations." e.g. "relations.ownedby: user:default/jane-doe". The value there must always be a valid entity reference. + +When querying for entity relationships, prefer using relations over spec fields. For example, use "relations.ownedby" instead of "spec.owner" to find entities owned by a particular group or user. + +## Query Syntax + +The query uses predicate expressions with dot-notation field paths. + +Simple matching: + { query: { kind: "Component" } } + { query: { kind: "Component", "spec.type": "service" } } + +Value operators: + { query: { kind: { "$in": ["API", "Component"] } } } + { query: { "metadata.annotations.backstage.io/techdocs-ref": { "$exists": true } } } + { query: { "metadata.tags": { "$contains": "java" } } } + { query: { "metadata.name": { "$hasPrefix": "team-" } } } + +Logical operators: + { query: { "$all": [{ kind: "Component" }, { "spec.lifecycle": "production" }] } } + { query: { "$any": [{ "spec.type": "service" }, { "spec.type": "website" }] } } + { query: { "$not": { kind: "Group" } } } + +Querying relations - find all entities owned by a specific group: + { query: { "relations.ownedby": "group:default/team-alpha" } } + +Combined example - find production services or websites with TechDocs: + { query: { "$all": [ + { kind: "Component", "spec.lifecycle": "production" }, + { "$any": [{ "spec.type": "service" }, { "spec.type": "website" }] }, + { "metadata.annotations.backstage.io/techdocs-ref": { "$exists": true } } + ] } } + +## Other Options + +Limit returned fields: { fields: ["kind", "metadata.name", "metadata.namespace"] } +Sort results: { orderFields: { field: "metadata.name", order: "asc" } } +Full text search: { fullTextFilter: { term: "auth", fields: ["metadata.name", "metadata.title"] } } +Pagination: Use limit (e.g. 20) and the returned nextPageCursor for subsequent requests via cursor. + `, + schema: { + input: z => + z.object({ + query: createZodV3FilterPredicateSchema(z) + .optional() + .describe( + 'Entity predicate query. Supports field matching, $all, $any, $not, $exists, $in, $contains, and $hasPrefix operators.', + ), + fields: z + .array(z.string()) + .optional() + .describe( + 'Specific fields to include in the response. If not provided, all fields are returned. Each entry is a dot separated path into an entity, e.g. `spec.type`.', + ), + limit: z + .number() + .int() + .positive() + .optional() + .describe('Maximum number of entities to return at a time.'), + offset: z + .number() + .int() + .min(0) + .optional() + .describe('Number of entities to skip before returning results.'), + orderFields: z + .union([ + z.object({ + field: z + .string() + .describe( + 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.', + ), + order: z.enum(['asc', 'desc']).describe('Sort order'), + }), + z.array( + z.object({ + field: z + .string() + .describe( + 'Field to order by. The format is a dot separated path into an entity, e.g. `spec.type`.', + ), + order: z.enum(['asc', 'desc']).describe('Sort order'), + }), + ), + ]) + .optional() + .describe( + 'Ordering criteria for the results. Can be a single order directive or an array for multi-field sorting.', + ), + fullTextFilter: z + .object({ + term: z.string().describe('Full text search term'), + fields: z + .array(z.string()) + .optional() + .describe( + 'Fields to search within. Each entry is a dot separated path into an entity, e.g. `spec.type`.', + ), + }) + .optional() + .describe('Full text search criteria'), + cursor: z + .string() + .optional() + .describe( + 'Cursor for pagination. This can be used only after the first request with a response containing a cursor. If a cursor is given it takes precedence over `offset`.', + ), + }), + output: z => + z.object({ + items: z + .array(z.object({}).passthrough()) + .describe('List of entities'), + totalItems: z.number().describe('Total number of entities'), + hasMoreEntities: z + .boolean() + .describe('Whether more entities are available'), + nextPageCursor: z + .string() + .optional() + .describe('Next page cursor used to fetch next page of entities'), + }), + }, + action: async ({ input, credentials }) => { + const response = await catalog.queryEntities( + { + ...input, + query: input.query, + }, + { credentials }, + ); + + return { + output: { + items: response.items, + totalItems: response.totalItems, + hasMoreEntities: !!response.pageInfo.nextCursor, + nextPageCursor: response.pageInfo.nextCursor, + }, + }; + }, + }); +}; diff --git a/plugins/catalog-backend/src/actions/createRegisterCatalogEntitiesAction.test.ts b/plugins/catalog-backend/src/actions/createRegisterCatalogEntitiesAction.test.ts index 31a89a31f3..a6926a2fd9 100644 --- a/plugins/catalog-backend/src/actions/createRegisterCatalogEntitiesAction.test.ts +++ b/plugins/catalog-backend/src/actions/createRegisterCatalogEntitiesAction.test.ts @@ -16,7 +16,6 @@ import { createRegisterCatalogEntitiesAction } from './createRegisterCatalogEntitiesAction'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; -import { ForwardedError } from '@backstage/errors'; describe('createRegisterCatalogEntitiesAction', () => { it('should successfully register a catalog location with a valid URL', async () => { @@ -78,14 +77,13 @@ describe('createRegisterCatalogEntitiesAction', () => { ).rejects.toThrow('not-a-valid-url is an invalid URL'); }); - it('should throw a ForwardedError if catalog.addLocation throws an error', async () => { + it('should throw the original error if catalog.addLocation fails', async () => { const mockActionsRegistry = actionsRegistryServiceMock(); const mockCatalog = catalogServiceMock(); - const errorMessage = 'Failed to add location'; mockCatalog.addLocation = jest .fn() - .mockRejectedValue(new Error(errorMessage)); + .mockRejectedValue(new Error('Failed to add location')); createRegisterCatalogEntitiesAction({ catalog: mockCatalog, @@ -100,6 +98,9 @@ describe('createRegisterCatalogEntitiesAction', () => { 'https://github.com/example/repo/blob/main/catalog-info.yaml', }, }), - ).rejects.toThrow(ForwardedError); + ).rejects.toMatchObject({ + name: 'Error', + message: 'Failed to add location', + }); }); }); diff --git a/plugins/catalog-backend/src/actions/createUnregisterCatalogEntitiesAction.test.ts b/plugins/catalog-backend/src/actions/createUnregisterCatalogEntitiesAction.test.ts index 492a427630..bd886e37ba 100644 --- a/plugins/catalog-backend/src/actions/createUnregisterCatalogEntitiesAction.test.ts +++ b/plugins/catalog-backend/src/actions/createUnregisterCatalogEntitiesAction.test.ts @@ -192,10 +192,13 @@ describe('createUnregisterCatalogEntitiesAction', () => { ); }); - it('should throw NotFoundError if no location matches the URL', async () => { + it('should throw NotFoundError with the original message if no location matches the URL', async () => { const mockActionsRegistry = actionsRegistryServiceMock(); const mockCatalog = catalogServiceMock(); + const locationUrl = + 'https://github.com/backstage/demo/blob/master/catalog-info.yaml'; + mockCatalog.getLocations = jest.fn().mockResolvedValue({ items: [ { id: 'location-id-1', target: 'https://other-url.com/catalog.yaml' }, @@ -207,51 +210,24 @@ describe('createUnregisterCatalogEntitiesAction', () => { actionsRegistry: mockActionsRegistry, }); - await expect( - mockActionsRegistry.invoke({ - id: 'test:unregister-entity', - input: { - type: { - locationUrl: - 'https://github.com/backstage/demo/blob/master/catalog-info.yaml', - }, - }, - }), - ).rejects.toThrow(/NotFoundError/); - }); - - it('should throw NotFoundError with descriptive message when location not found', async () => { - const mockActionsRegistry = actionsRegistryServiceMock(); - const mockCatalog = catalogServiceMock(); - - const locationUrl = - 'https://github.com/backstage/demo/blob/master/catalog-info.yaml'; - - mockCatalog.getLocations = jest.fn().mockResolvedValue({ - items: [], - }); - - createUnregisterCatalogEntitiesAction({ - catalog: mockCatalog, - actionsRegistry: mockActionsRegistry, - }); - await expect( mockActionsRegistry.invoke({ id: 'test:unregister-entity', input: { type: { locationUrl } }, }), - ).rejects.toThrow(`Location with URL ${locationUrl} not found`); + ).rejects.toMatchObject({ + name: 'NotFoundError', + message: `Location with URL ${locationUrl} not found`, + }); }); - it('should throw an error if catalog.getLocations throws an error', async () => { + it('should throw the original error if catalog.getLocations fails', async () => { const mockActionsRegistry = actionsRegistryServiceMock(); const mockCatalog = catalogServiceMock(); - const errorMessage = 'Failed to get locations'; mockCatalog.getLocations = jest .fn() - .mockRejectedValue(new Error(errorMessage)); + .mockRejectedValue(new Error('Failed to get locations')); createUnregisterCatalogEntitiesAction({ catalog: mockCatalog, @@ -268,7 +244,10 @@ describe('createUnregisterCatalogEntitiesAction', () => { }, }, }), - ).rejects.toThrow(errorMessage); + ).rejects.toMatchObject({ + name: 'Error', + message: 'Failed to get locations', + }); }); }); }); diff --git a/plugins/catalog-backend/src/actions/index.ts b/plugins/catalog-backend/src/actions/index.ts index 5940bc32f1..04587c1a4e 100644 --- a/plugins/catalog-backend/src/actions/index.ts +++ b/plugins/catalog-backend/src/actions/index.ts @@ -19,6 +19,7 @@ import { createGetCatalogEntityAction } from './createGetCatalogEntityAction.ts' import { createValidateEntityAction } from './createValidateEntityAction.ts'; import { createRegisterCatalogEntitiesAction } from './createRegisterCatalogEntitiesAction.ts'; import { createUnregisterCatalogEntitiesAction } from './createUnregisterCatalogEntitiesAction.ts'; +import { createQueryCatalogEntitiesAction } from './createQueryCatalogEntitiesAction.ts'; export const createCatalogActions = (options: { actionsRegistry: ActionsRegistryService; @@ -28,4 +29,5 @@ export const createCatalogActions = (options: { createValidateEntityAction(options); createRegisterCatalogEntitiesAction(options); createUnregisterCatalogEntitiesAction(options); + createQueryCatalogEntitiesAction(options); }; diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 1fbf337986..3d6aff55ba 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -17,6 +17,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { FilterPredicate } from '@backstage/filter-predicates'; /** * A pagination rule for entities. @@ -85,6 +86,10 @@ export interface EntitiesBatchRequest { * they did not exist. */ filter?: EntityFilter; + /** + * Predicate-based query for filtering entities. + */ + query?: FilterPredicate; /** * Strips out only the parts of the entity bodies to include in the response. */ @@ -119,6 +124,10 @@ export interface EntityFacetsRequest { * A filter to apply on the full list of entities before computing the facets. */ filter?: EntityFilter; + /** + * Predicate-based query for filtering entities. + */ + query?: FilterPredicate; /** * The facets to compute. * @@ -212,6 +221,10 @@ export interface QueryEntitiesInitialRequest { limit?: number; offset?: number; filter?: EntityFilter; + /** + * Predicate-based query for filtering entities. + */ + query?: FilterPredicate; orderFields?: EntityOrder[]; fullTextFilter?: { term: string; @@ -273,6 +286,10 @@ export type Cursor = { * A filter to be applied to the full list of entities. */ filter?: EntityFilter; + /** + * A predicate-based query to be applied to the full list of entities. + */ + query?: FilterPredicate; /** * true if the cursor is a previous cursor. */ diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 790762a428..b9f3599b19 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -36,6 +36,7 @@ import { createRandomProcessingInterval } from '../processing/refresh'; import { timestampToDateTime } from './conversion'; import { generateStableHash } from './util'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -59,6 +60,7 @@ describe('DefaultProcessingDatabase', () => { maxSeconds: 150, }), events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }), }; } diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index a421ca19b2..264c11055d 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -47,6 +47,7 @@ import { DateTime } from 'luxon'; import { CATALOG_CONFLICTS_TOPIC } from '../constants'; import { CatalogConflictEventPayload } from '../catalog/types'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; // The number of items that are sent per batch to the database layer, when // doing .batchInsert calls to knex. This needs to be low enough to not cause @@ -60,6 +61,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { logger: LoggerService; refreshInterval: ProcessingIntervalFunction; events: EventsService; + metrics: MetricsService; }; constructor(options: { @@ -67,9 +69,10 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { logger: LoggerService; refreshInterval: ProcessingIntervalFunction; events: EventsService; + metrics: MetricsService; }) { this.options = options; - initDatabaseMetrics(options.database); + initDatabaseMetrics(options.database, options.metrics); } async updateProcessedEntity( diff --git a/plugins/catalog-backend/src/database/metrics.ts b/plugins/catalog-backend/src/database/metrics.ts index 809405d69f..f6a1464b3a 100644 --- a/plugins/catalog-backend/src/database/metrics.ts +++ b/plugins/catalog-backend/src/database/metrics.ts @@ -17,12 +17,12 @@ import { Knex } from 'knex'; import { createGaugeMetric } from '../util/metrics'; import { DbRelationsRow, DbLocationsRow, DbSearchRow } from './tables'; -import { metrics } from '@opentelemetry/api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; -export function initDatabaseMetrics(knex: Knex) { +export function initDatabaseMetrics(knex: Knex, metrics: MetricsService) { const seenProm = new Set(); const seen = new Set(); - const meter = metrics.getMeter('default'); + return { entities_count_prom: createGaugeMetric({ name: 'catalog_entities_count', @@ -69,7 +69,7 @@ export function initDatabaseMetrics(knex: Knex) { this.set(Number(total[0].count)); }, }), - entities_count: meter + entities_count: metrics .createObservableGauge('catalog_entities_count', { description: 'Total amount of entities in the catalog', }) @@ -93,7 +93,7 @@ export function initDatabaseMetrics(knex: Knex) { } }); }), - registered_locations: meter + registered_locations: metrics .createObservableGauge('catalog_registered_locations_count', { description: 'Total amount of registered locations in the catalog', }) @@ -113,7 +113,7 @@ export function initDatabaseMetrics(knex: Knex) { gauge.observe(Number(total[0].count)); } }), - relations: meter + relations: metrics .createObservableGauge('catalog_relations_count', { description: 'Total amount of relations between entities', }) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.test.ts index b66dc74ef2..1f6137f91e 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.test.ts @@ -16,6 +16,7 @@ import { TestDatabases } from '@backstage/backend-test-utils'; import { applyDatabaseMigrations } from '../../migrations'; +import { DbStitchQueueRow } from '../../tables'; import { getDeferredStitchableEntities } from './getDeferredStitchableEntities'; jest.setTimeout(60_000); @@ -29,56 +30,27 @@ describe('getDeferredStitchableEntities', () => { const knex = await databases.init(databaseId); await applyDatabaseMigrations(knex); - await knex - .insert([ - { - entity_id: '1', - entity_ref: 'k:ns/no_stitch_time', - unprocessed_entity: '{}', - processed_entity: '{}', - errors: '[]', - next_update_at: knex.fn.now(), - last_discovery_at: knex.fn.now(), - next_stitch_at: null, - next_stitch_ticket: null, - }, - { - entity_id: '2', - entity_ref: 'k:ns/future_stitch_time', - unprocessed_entity: '{}', - processed_entity: '{}', - errors: '[]', - next_update_at: knex.fn.now(), - last_discovery_at: knex.fn.now(), - next_stitch_at: '2037-01-01T00:00:00.000', - next_stitch_ticket: 't1', - }, - { - entity_id: '3', - entity_ref: 'k:ns/past_stitch_time', - unprocessed_entity: '{}', - processed_entity: '{}', - errors: '[]', - next_update_at: knex.fn.now(), - last_discovery_at: knex.fn.now(), - next_stitch_at: '1971-01-01T00:00:00.000', - next_stitch_ticket: 't3', - }, - { - entity_id: '4', - entity_ref: 'k:ns/past_stitch_time_again', - unprocessed_entity: '{}', - processed_entity: '{}', - errors: '[]', - next_update_at: knex.fn.now(), - last_discovery_at: knex.fn.now(), - next_stitch_at: '1972-01-01T00:00:00.000', - next_stitch_ticket: 't4', - }, - ]) - .into('refresh_state'); + // Insert stitch_queue rows - no need for refresh_state rows since + // stitch_queue is a standalone table + await knex('stitch_queue').insert([ + { + entity_ref: 'k:ns/future_stitch_time', + stitch_ticket: 't1', + next_stitch_at: '2037-01-01T00:00:00.000', + }, + { + entity_ref: 'k:ns/past_stitch_time', + stitch_ticket: 't3', + next_stitch_at: '1971-01-01T00:00:00.000', + }, + { + entity_ref: 'k:ns/past_stitch_time_again', + stitch_ticket: 't4', + next_stitch_at: '1972-01-01T00:00:00.000', + }, + ]); - const rowsBefore = await knex('refresh_state'); + const rowsBefore = await knex('stitch_queue'); const items = await getDeferredStitchableEntities({ knex, @@ -86,7 +58,7 @@ describe('getDeferredStitchableEntities', () => { stitchTimeout: { seconds: 2 }, }); - const rowsAfter = await knex('refresh_state'); + const rowsAfter = await knex('stitch_queue'); expect(items).toEqual([ { @@ -96,17 +68,21 @@ describe('getDeferredStitchableEntities', () => { }, ]); - const hitRowBefore = rowsBefore.filter(r => r.entity_id === '3')[0] - .next_stitch_at; - const hitRowAfter = rowsAfter.filter(r => r.entity_id === '3')[0] - .next_stitch_at; - const missRowBefore = rowsBefore.filter(r => r.entity_id === '4')[0] - .next_stitch_at; - const missRowAfter = rowsAfter.filter(r => r.entity_id === '4')[0] - .next_stitch_at; + const hitRowBefore = rowsBefore.filter( + r => r.entity_ref === 'k:ns/past_stitch_time', + )[0].next_stitch_at; + const hitRowAfter = rowsAfter.filter( + r => r.entity_ref === 'k:ns/past_stitch_time', + )[0].next_stitch_at; + const missRowBefore = rowsBefore.filter( + r => r.entity_ref === 'k:ns/past_stitch_time_again', + )[0].next_stitch_at; + const missRowAfter = rowsAfter.filter( + r => r.entity_ref === 'k:ns/past_stitch_time_again', + )[0].next_stitch_at; - expect(+new Date(hitRowAfter)).toBeGreaterThan(+new Date(hitRowBefore)); - expect(+new Date(missRowAfter)).toEqual(+new Date(missRowBefore)); + expect(+new Date(hitRowAfter!)).toBeGreaterThan(+new Date(hitRowBefore!)); + expect(+new Date(missRowAfter!)).toEqual(+new Date(missRowBefore!)); }, ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.ts b/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.ts index 05c3f4d2eb..2d12ee4f6a 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/getDeferredStitchableEntities.ts @@ -18,7 +18,7 @@ import { durationToMilliseconds, HumanDuration } from '@backstage/types'; import { Knex } from 'knex'; import { DateTime } from 'luxon'; import { timestampToDateTime } from '../../conversion'; -import { DbRefreshStateRow } from '../../tables'; +import { DbStitchQueueRow } from '../../tables'; // TODO(freben): There is no retry counter or similar. If items start // perpetually crashing during stitching, they'll just get silently retried over @@ -31,7 +31,7 @@ import { DbRefreshStateRow } from '../../tables'; * * This assumes that the stitching strategy is set to deferred. * - * They are expected to already have the next_stitch_ticket set (by + * They are expected to already have the stitch_ticket set (by * markForStitching) so that their tickets can be returned with each item. * * All returned items have their next_stitch_at updated to be moved forward by @@ -52,10 +52,10 @@ export async function getDeferredStitchableEntities(options: { > { const { knex, batchSize, stitchTimeout } = options; - let itemsQuery = knex('refresh_state').select( + let itemsQuery = knex('stitch_queue').select( 'entity_ref', 'next_stitch_at', - 'next_stitch_ticket', + 'stitch_ticket', ); // This avoids duplication of work because of race conditions and is @@ -66,8 +66,6 @@ export async function getDeferredStitchableEntities(options: { } const items = await itemsQuery - .whereNotNull('next_stitch_at') - .whereNotNull('next_stitch_ticket') .where('next_stitch_at', '<=', knex.fn.now()) .orderBy('next_stitch_at', 'asc') .limit(batchSize); @@ -76,21 +74,19 @@ export async function getDeferredStitchableEntities(options: { return []; } - await knex('refresh_state') + await knex('stitch_queue') .whereIn( 'entity_ref', items.map(i => i.entity_ref), ) - // avoid race condition where someone completes a stitch right between these statements - .whereNotNull('next_stitch_ticket') .update({ next_stitch_at: nowPlus(knex, stitchTimeout), }); return items.map(i => ({ entityRef: i.entity_ref, - stitchTicket: i.next_stitch_ticket!, - stitchRequestedAt: timestampToDateTime(i.next_stitch_at!), + stitchTicket: i.stitch_ticket, + stitchRequestedAt: timestampToDateTime(i.next_stitch_at), })); } diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.test.ts index b0957e0ec9..dbcbba13df 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.test.ts @@ -17,7 +17,7 @@ import { TestDatabases } from '@backstage/backend-test-utils'; import { applyDatabaseMigrations } from '../../migrations'; import { markDeferredStitchCompleted } from './markDeferredStitchCompleted'; -import { DbRefreshStateRow } from '../../tables'; +import { DbStitchQueueRow } from '../../tables'; jest.setTimeout(60_000); @@ -30,44 +30,44 @@ describe('markDeferredStitchCompleted', () => { const knex = await databases.init(databaseId); await applyDatabaseMigrations(knex); - await knex('refresh_state').insert([ + // Insert stitch_queue row + await knex('stitch_queue').insert([ { - entity_id: '1', entity_ref: 'k:ns/n', - unprocessed_entity: '{}', - processed_entity: '{}', - errors: '[]', - next_update_at: knex.fn.now(), - last_discovery_at: knex.fn.now(), + stitch_ticket: 'the-ticket', next_stitch_at: '1971-01-01T00:00:00.000', - next_stitch_ticket: 'the-ticket', }, ]); async function result() { - return knex('refresh_state').select( + return knex('stitch_queue').select( + 'entity_ref', 'next_stitch_at', - 'next_stitch_ticket', + 'stitch_ticket', ); } + // Wrong ticket should not delete the row await markDeferredStitchCompleted({ knex, entityRef: 'k:ns/n', stitchTicket: 'the-wrong-ticket', }); await expect(result()).resolves.toEqual([ - { next_stitch_at: expect.anything(), next_stitch_ticket: 'the-ticket' }, + { + entity_ref: 'k:ns/n', + next_stitch_at: expect.anything(), + stitch_ticket: 'the-ticket', + }, ]); + // Correct ticket should delete the row await markDeferredStitchCompleted({ knex, entityRef: 'k:ns/n', stitchTicket: 'the-ticket', }); - await expect(result()).resolves.toEqual([ - { next_stitch_at: null, next_stitch_ticket: null }, - ]); + await expect(result()).resolves.toEqual([]); }, ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.ts b/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.ts index d1c7c3a6b8..bb3e2be6f9 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markDeferredStitchCompleted.ts @@ -15,7 +15,7 @@ */ import { Knex } from 'knex'; -import { DbRefreshStateRow } from '../../tables'; +import { DbStitchQueueRow } from '../../tables'; /** * Marks a single entity as having been stitched. @@ -24,10 +24,10 @@ import { DbRefreshStateRow } from '../../tables'; * * This assumes that the stitching strategy is set to deferred. * - * The timestamp and ticket are only reset if the ticket hasn't changed. If it - * has, it means that a new stitch request has been made, and the entity should - * be stitched once more some time in the future - or is indeed already being - * stitched concurrently with ourselves. + * The row is only deleted from stitch_queue if the ticket hasn't changed. If + * it has, it means that a new stitch request has been made, and the entity + * should be stitched once more some time in the future - or is indeed already + * being stitched concurrently with ourselves. */ export async function markDeferredStitchCompleted(option: { knex: Knex | Knex.Transaction; @@ -36,11 +36,8 @@ export async function markDeferredStitchCompleted(option: { }): Promise { const { knex, entityRef, stitchTicket } = option; - await knex('refresh_state') - .update({ - next_stitch_at: null, - next_stitch_ticket: null, - }) + await knex('stitch_queue') .where('entity_ref', '=', entityRef) - .andWhere('next_stitch_ticket', '=', stitchTicket); + .andWhere('stitch_ticket', '=', stitchTicket) + .delete(); } diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts index ff450e9ccf..f7fd9a0695 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts @@ -17,7 +17,11 @@ import { TestDatabases } from '@backstage/backend-test-utils'; import { applyDatabaseMigrations } from '../../migrations'; import { markForStitching } from './markForStitching'; -import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; +import { + DbFinalEntitiesRow, + DbRefreshStateRow, + DbStitchQueueRow, +} from '../../tables'; jest.setTimeout(60_000); @@ -39,8 +43,6 @@ describe('markForStitching', () => { errors: '[]', next_update_at: knex.fn.now(), last_discovery_at: knex.fn.now(), - next_stitch_at: null, - next_stitch_ticket: null, }, { entity_id: '2', @@ -50,8 +52,6 @@ describe('markForStitching', () => { errors: '[]', next_update_at: knex.fn.now(), last_discovery_at: knex.fn.now(), - next_stitch_at: null, - next_stitch_ticket: null, }, { entity_id: '3', @@ -61,8 +61,6 @@ describe('markForStitching', () => { errors: '[]', next_update_at: knex.fn.now(), last_discovery_at: knex.fn.now(), - next_stitch_at: null, - next_stitch_ticket: null, }, { entity_id: '4', @@ -72,19 +70,34 @@ describe('markForStitching', () => { errors: '[]', next_update_at: knex.fn.now(), last_discovery_at: knex.fn.now(), + }, + ]); + // Entity 4 has an existing stitch_queue row with old stitch data + await knex('stitch_queue').insert([ + { + entity_ref: 'k:ns/four', + stitch_ticket: 'old', next_stitch_at: '1971-01-01T00:00:00.000', - next_stitch_ticket: 'old', }, ]); async function result() { - return knex('refresh_state') - .select('entity_id', 'next_stitch_at', 'next_stitch_ticket') - .orderBy('entity_id', 'asc'); + return knex('stitch_queue') + .select('entity_ref', 'next_stitch_at', 'stitch_ticket') + .orderBy('entity_ref', 'asc'); } + // Initially only entity 4 has a stitch_queue row const original = await result(); + expect(original).toEqual([ + { + entity_ref: 'k:ns/four', + next_stitch_at: expect.anything(), + stitch_ticket: 'old', + }, + ]); + // Calling with empty set should not create any new rows await markForStitching({ knex, strategy: { @@ -95,16 +108,14 @@ describe('markForStitching', () => { entityRefs: new Set(), }); await expect(result()).resolves.toEqual([ - { entity_id: '1', next_stitch_at: null, next_stitch_ticket: null }, - { entity_id: '2', next_stitch_at: null, next_stitch_ticket: null }, - { entity_id: '3', next_stitch_at: null, next_stitch_ticket: null }, { - entity_id: '4', + entity_ref: 'k:ns/four', next_stitch_at: expect.anything(), - next_stitch_ticket: 'old', + stitch_ticket: 'old', }, ]); + // Mark entity 1 - should create a new stitch_queue row await markForStitching({ knex, strategy: { @@ -116,19 +127,18 @@ describe('markForStitching', () => { }); await expect(result()).resolves.toEqual([ { - entity_id: '1', + entity_ref: 'k:ns/four', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: 'old', }, - { entity_id: '2', next_stitch_at: null, next_stitch_ticket: null }, - { entity_id: '3', next_stitch_at: null, next_stitch_ticket: null }, { - entity_id: '4', + entity_ref: 'k:ns/one', next_stitch_at: expect.anything(), - next_stitch_ticket: 'old', + stitch_ticket: expect.anything(), }, ]); + // Mark entity 2 - should create another new stitch_queue row await markForStitching({ knex, strategy: { @@ -140,23 +150,23 @@ describe('markForStitching', () => { }); await expect(result()).resolves.toEqual([ { - entity_id: '1', + entity_ref: 'k:ns/four', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: 'old', }, { - entity_id: '2', + entity_ref: 'k:ns/one', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: expect.anything(), }, - { entity_id: '3', next_stitch_at: null, next_stitch_ticket: null }, { - entity_id: '4', + entity_ref: 'k:ns/two', next_stitch_at: expect.anything(), - next_stitch_ticket: 'old', + stitch_ticket: expect.anything(), }, ]); + // Mark entities 3 and 4 by ID - entity 3 creates new row, entity 4 updates existing await markForStitching({ knex, strategy: { @@ -168,35 +178,31 @@ describe('markForStitching', () => { }); await expect(result()).resolves.toEqual([ { - entity_id: '1', + entity_ref: 'k:ns/four', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: expect.anything(), }, { - entity_id: '2', + entity_ref: 'k:ns/one', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: expect.anything(), }, { - entity_id: '3', + entity_ref: 'k:ns/three', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: expect.anything(), }, { - entity_id: '4', + entity_ref: 'k:ns/two', next_stitch_at: expect.anything(), - next_stitch_ticket: expect.anything(), + stitch_ticket: expect.anything(), }, ]); - // It overwrites timers and tickets if they existed before + // Entity 4's ticket should have been updated (was 'old', now something else) const final = await result(); - for (let i = 0; i < final.length; ++i) { - expect(original[i].next_stitch_at).not.toEqual(final[i].next_stitch_at); - expect(original[i].next_stitch_ticket).not.toEqual( - final[i].next_stitch_ticket, - ); - } + const entity4Final = final.find(r => r.entity_ref === 'k:ns/four'); + expect(entity4Final?.stitch_ticket).not.toEqual('old'); }, ); @@ -254,28 +260,24 @@ describe('markForStitching', () => { final_entity: '{}', entity_ref: 'k:ns/one', hash: 'old', - stitch_ticket: 'old', }, { entity_id: '2', final_entity: '{}', entity_ref: 'k:ns/two', hash: 'old', - stitch_ticket: 'old', }, { entity_id: '3', final_entity: '{}', entity_ref: 'k:ns/three', hash: 'old', - stitch_ticket: 'old', }, { entity_id: '4', final_entity: '{}', entity_ref: 'k:ns/four', hash: 'old', - stitch_ticket: 'old', }, ]); @@ -461,8 +463,6 @@ describe('markForStitching', () => { errors: '[]', next_update_at: knex.fn.now(), last_discovery_at: knex.fn.now(), - next_stitch_at: null, - next_stitch_ticket: null, })), ); @@ -558,15 +558,14 @@ describe('markForStitching', () => { expect(deadlockErrors).toEqual([]); // Verify final state - all entities should have been marked for stitching - const finalState = await knex('refresh_state') - .select('entity_ref', 'next_stitch_at', 'next_stitch_ticket') - .whereNotNull('next_stitch_at') + const finalState = await knex('stitch_queue') + .select('entity_ref', 'next_stitch_at', 'stitch_ticket') .orderBy('entity_ref'); expect(finalState.length).toBeGreaterThan(0); finalState.forEach(row => { expect(row.next_stitch_at).not.toBeNull(); - expect(row.next_stitch_ticket).not.toBeNull(); + expect(row.stitch_ticket).not.toBeNull(); }); }, ); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index d26fb8d6d4..9e22a65e7b 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -17,33 +17,15 @@ import { Knex } from 'knex'; import splitToChunks from 'lodash/chunk'; import { v4 as uuid } from 'uuid'; -import { ErrorLike, isError } from '@backstage/errors'; import { StitchingStrategy } from '../../../stitching/types'; -import { setTimeout as sleep } from 'node:timers/promises'; -import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; +import { + DbFinalEntitiesRow, + DbRefreshStateRow, + DbStitchQueueRow, +} from '../../tables'; +import { retryOnDeadlock } from '../../util'; const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention -const DEADLOCK_RETRY_ATTEMPTS = 3; -const DEADLOCK_BASE_DELAY_MS = 25; - -// PostgreSQL deadlock error code -const POSTGRES_DEADLOCK_SQLSTATE = '40P01'; - -/** - * Checks if the given error is a deadlock error for the database engine in use. - */ -function isDeadlockError( - knex: Knex | Knex.Transaction, - e: unknown, -): e is ErrorLike { - if (knex.client.config.client.includes('pg')) { - // PostgreSQL deadlock detection - return isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE; - } - - // Add more database engine checks here as needed - return false; -} /** * Marks a number of entities for stitching some time in the near @@ -69,12 +51,7 @@ export async function markForStitching(options: { .update({ hash: 'force-stitching', }) - .whereIn( - 'entity_id', - knex('refresh_state') - .select('entity_id') - .whereIn('entity_ref', chunk), - ); + .whereIn('entity_ref', chunk); await retryOnDeadlock(async () => { await knex .table('refresh_state') @@ -104,30 +81,46 @@ export async function markForStitching(options: { }, knex); } } else if (mode === 'deferred') { - // It's OK that this is shared across refresh state rows; it just needs to + // It's OK that this is shared across stitch_queue rows; it just needs to // be uniquely generated for every new stitch request. const ticket = uuid(); - // Update by primary key in deterministic order to avoid deadlocks for (const chunk of entityRefs) { await retryOnDeadlock(async () => { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_ref', chunk); + if (chunk.length > 0) { + await knex('stitch_queue') + .insert( + chunk.map(ref => ({ + entity_ref: ref, + stitch_ticket: ticket, + next_stitch_at: knex.fn.now(), + })), + ) + .onConflict('entity_ref') + .merge(['next_stitch_at', 'stitch_ticket']); + } }, knex); } for (const chunk of entityIds) { await retryOnDeadlock(async () => { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) + // Look up entity_refs from refresh_state by entity_id + const refreshStateRows = await knex('refresh_state') + .select('entity_ref') .whereIn('entity_id', chunk); + + if (refreshStateRows.length > 0) { + await knex('stitch_queue') + .insert( + refreshStateRows.map(row => ({ + entity_ref: row.entity_ref, + stitch_ticket: ticket, + next_stitch_at: knex.fn.now(), + })), + ) + .onConflict('entity_ref') + .merge(['next_stitch_at', 'stitch_ticket']); + } }, knex); } } else { @@ -143,24 +136,3 @@ function sortSplit(input: Iterable | undefined): string[][] { array.sort(); return splitToChunks(array, UPDATE_CHUNK_SIZE); } - -async function retryOnDeadlock( - fn: () => Promise, - knex: Knex | Knex.Transaction, - retries = DEADLOCK_RETRY_ATTEMPTS, - baseMs = DEADLOCK_BASE_DELAY_MS, -): Promise { - let attempt = 0; - for (;;) { - try { - return await fn(); - } catch (e: unknown) { - if (isDeadlockError(knex, e) && attempt < retries) { - await sleep(baseMs * Math.pow(2, attempt)); - attempt++; - continue; - } - throw e; - } - } -} diff --git a/plugins/catalog-backend/src/database/operations/stitcher/performStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/performStitching.test.ts index 985146229a..41931a8b7e 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/performStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/performStitching.test.ts @@ -24,6 +24,7 @@ import { DbRelationsRow, DbSearchRow, } from '../../tables'; +import { markForStitching } from './markForStitching'; import { performStitching } from './performStitching'; jest.setTimeout(60_000); @@ -82,15 +83,29 @@ describe('performStitching', () => { }, ]); + const deferredStrategy = { + mode: 'deferred' as const, + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }; + + await markForStitching({ + knex, + strategy: deferredStrategy, + entityRefs: ['k:ns/n'], + }); + await performStitching({ knex, logger, - strategy: { - mode: 'deferred', - pollingInterval: { seconds: 1 }, - stitchTimeout: { seconds: 1 }, - }, + strategy: deferredStrategy, entityRef: 'k:ns/n', + stitchTicket: ( + await knex('stitch_queue') + .where('entity_ref', 'k:ns/n') + .select('stitch_ticket') + .first() + )?.stitch_ticket, }); entities = await knex('final_entities'); @@ -171,15 +186,23 @@ describe('performStitching', () => { ); // Re-stitch without any changes + await markForStitching({ + knex, + strategy: deferredStrategy, + entityRefs: ['k:ns/n'], + }); + await performStitching({ knex, logger, - strategy: { - mode: 'deferred', - pollingInterval: { seconds: 1 }, - stitchTimeout: { seconds: 1 }, - }, + strategy: deferredStrategy, entityRef: 'k:ns/n', + stitchTicket: ( + await knex('stitch_queue') + .where('entity_ref', 'k:ns/n') + .select('stitch_ticket') + .first() + )?.stitch_ticket, }); entities = await knex('final_entities'); @@ -198,15 +221,23 @@ describe('performStitching', () => { }, ]); + await markForStitching({ + knex, + strategy: deferredStrategy, + entityRefs: ['k:ns/n'], + }); + await performStitching({ knex, logger, - strategy: { - mode: 'deferred', - pollingInterval: { seconds: 1 }, - stitchTimeout: { seconds: 1 }, - }, + strategy: deferredStrategy, entityRef: 'k:ns/n', + stitchTicket: ( + await knex('stitch_queue') + .where('entity_ref', 'k:ns/n') + .select('stitch_ticket') + .first() + )?.stitch_ticket, }); entities = await knex('final_entities'); @@ -342,7 +373,6 @@ describe('performStitching', () => { entity_id: 'my-id', entity_ref: 'k:ns/n', hash: '', - stitch_ticket: 'old-ticket', final_entity: JSON.stringify({}), }, ]); @@ -365,7 +395,7 @@ describe('performStitching', () => { ); it.each(databases.eachSupportedId())( - 'replaces existing stitch ticket %p', + 'stitches when final_entities row already exists %p', async databaseId => { const knex = await databases.init(databaseId); await applyDatabaseMigrations(knex); @@ -396,23 +426,10 @@ describe('performStitching', () => { entity_id: 'my-id', entity_ref: 'k:ns/n', hash: '', - stitch_ticket: 'old-ticket', final_entity: JSON.stringify({}), }, ]); - await expect( - knex('final_entities').select([ - 'entity_id', - 'stitch_ticket', - ]), - ).resolves.toEqual([ - { - entity_id: 'my-id', - stitch_ticket: expect.stringContaining('old-ticket'), - }, - ]); - const stitchLogger = mockServices.logger.mock(); await expect( performStitching({ @@ -423,17 +440,10 @@ describe('performStitching', () => { }), ).resolves.toBe('changed'); - await expect( - knex('final_entities').select([ - 'entity_id', - 'stitch_ticket', - ]), - ).resolves.toEqual([ - { - entity_id: 'my-id', - stitch_ticket: expect.not.stringContaining('old-ticket'), - }, - ]); + const entities = await knex('final_entities'); + expect(entities.length).toBe(1); + expect(entities[0].hash).not.toBe(''); + expect(entities[0].final_entity).toBeDefined(); }, ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/performStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/performStitching.ts index 32c0d4ccb3..95fe880eda 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/performStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/performStitching.ts @@ -23,12 +23,12 @@ import { import { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha'; import { SerializedError } from '@backstage/errors'; import { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; import { StitchingStrategy } from '../../../stitching/types'; import { DbFinalEntitiesRow, DbRefreshStateRow, DbSearchRow, + DbStitchQueueRow, } from '../../tables'; import { buildEntitySearch } from './buildEntitySearch'; import { markDeferredStitchCompleted } from './markDeferredStitchCompleted'; @@ -56,7 +56,7 @@ export async function performStitching(options: { stitchTicket?: string; }): Promise<'changed' | 'unchanged' | 'abandoned'> { const { knex, logger, entityRef } = options; - const stitchTicket = options.stitchTicket ?? uuid(); + const stitchTicket = options.stitchTicket; // In deferred mode, the entity is removed from the stitch queue on ANY // completion, except when an exception is thrown. In the latter case, the @@ -73,17 +73,16 @@ export async function performStitching(options: { return 'abandoned'; } - // Insert stitching ticket that will be compared before inserting the final entity. + // Ensure that a final_entities row exists for this entity. try { await knex('final_entities') .insert({ entity_id: entityResult[0].entity_id, hash: '', entity_ref: entityRef, - stitch_ticket: stitchTicket, }) .onConflict('entity_id') - .merge(['stitch_ticket']); + .ignore(); } catch (error) { // It's possible to hit a race where a refresh_state table delete + insert // is done just after we read the entity_id from it. This conflict is safe @@ -231,14 +230,26 @@ export async function performStitching(options: { // to write the search index. const searchEntries = buildEntitySearch(entityId, entity); - const amountOfRowsChanged = await knex('final_entities') + let updateQuery = knex('final_entities') .update({ final_entity: JSON.stringify(entity), hash, last_updated_at: knex.fn.now(), }) - .where('entity_id', entityId) - .where('stitch_ticket', stitchTicket); + .where('entity_id', entityId); + + // In deferred mode, guard against concurrent stitchers by checking that + // the stitch_ticket in stitch_queue still matches what we were given. + if (options.strategy.mode === 'deferred' && stitchTicket) { + updateQuery = updateQuery.whereExists( + knex('stitch_queue') + .where('stitch_queue.entity_ref', entityRef) + .where('stitch_queue.stitch_ticket', stitchTicket) + .select(knex.raw('1')), + ); + } + + const amountOfRowsChanged = await updateQuery; if (amountOfRowsChanged === 0) { logger.debug(`Entity ${entityRef} is already stitched, skipping write.`); @@ -255,7 +266,7 @@ export async function performStitching(options: { removeFromStitchQueueOnCompletion = false; throw error; } finally { - if (removeFromStitchQueueOnCompletion) { + if (removeFromStitchQueueOnCompletion && stitchTicket) { await markDeferredStitchCompleted({ knex: knex, entityRef, diff --git a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.test.ts b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.test.ts index 0f4f5f0c3d..c61c425a30 100644 --- a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.test.ts +++ b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.test.ts @@ -70,7 +70,6 @@ describe('deleteOrphanedEntities', () => { entity_id: `id-${ref}`, hash: 'original', entity_ref: ref, - stitch_ticket: '', }); } } @@ -100,7 +99,13 @@ describe('deleteOrphanedEntities', () => { async function refreshState(knex: Knex) { return await knex('refresh_state') .orderBy('entity_ref') - .select('entity_ref', 'result_hash', 'next_stitch_at'); + .select('entity_ref', 'result_hash'); + } + + async function stitchQueue(knex: Knex) { + return await knex('stitch_queue') + .orderBy('entity_ref') + .select('entity_ref'); } async function finalEntities(knex: Knex) { @@ -110,11 +115,16 @@ describe('deleteOrphanedEntities', () => { 'final_entities.entity_id', 'refresh_state.entity_id', ) + .leftOuterJoin( + 'stitch_queue', + 'stitch_queue.entity_ref', + 'refresh_state.entity_ref', + ) .orderBy('refresh_state.entity_ref') .select({ entity_ref: 'refresh_state.entity_ref', hash: 'final_entities.hash', - next_stitch_at: 'refresh_state.next_stitch_at', + next_stitch_at: 'stitch_queue.next_stitch_at', }); } @@ -178,20 +188,13 @@ describe('deleteOrphanedEntities', () => { await insertRelation(knex, 'E7', 'E6'); await expect(run(knex, { mode: 'immediate' })).resolves.toEqual(5); await expect(refreshState(knex)).resolves.toEqual([ - { entity_ref: 'E1', result_hash: 'original', next_stitch_at: null }, - { - entity_ref: 'E2', - result_hash: 'force-stitching', - next_stitch_at: null, - }, - { - entity_ref: 'E7', - result_hash: 'force-stitching', - next_stitch_at: null, - }, - { entity_ref: 'E8', result_hash: 'original', next_stitch_at: null }, - { entity_ref: 'E9', result_hash: 'original', next_stitch_at: null }, + { entity_ref: 'E1', result_hash: 'original' }, + { entity_ref: 'E2', result_hash: 'force-stitching' }, + { entity_ref: 'E7', result_hash: 'force-stitching' }, + { entity_ref: 'E8', result_hash: 'original' }, + { entity_ref: 'E9', result_hash: 'original' }, ]); + await expect(stitchQueue(knex)).resolves.toEqual([]); await expect(finalEntities(knex)).resolves.toEqual([ { entity_ref: 'E1', hash: 'original', next_stitch_at: null }, { @@ -276,19 +279,15 @@ describe('deleteOrphanedEntities', () => { }), ).resolves.toEqual(5); await expect(refreshState(knex)).resolves.toEqual([ - { entity_ref: 'E1', result_hash: 'original', next_stitch_at: null }, - { - entity_ref: 'E2', - result_hash: 'original', - next_stitch_at: expect.anything(), - }, - { - entity_ref: 'E7', - result_hash: 'original', - next_stitch_at: expect.anything(), - }, - { entity_ref: 'E8', result_hash: 'original', next_stitch_at: null }, - { entity_ref: 'E9', result_hash: 'original', next_stitch_at: null }, + { entity_ref: 'E1', result_hash: 'original' }, + { entity_ref: 'E2', result_hash: 'original' }, + { entity_ref: 'E7', result_hash: 'original' }, + { entity_ref: 'E8', result_hash: 'original' }, + { entity_ref: 'E9', result_hash: 'original' }, + ]); + await expect(stitchQueue(knex)).resolves.toEqual([ + { entity_ref: 'E2' }, + { entity_ref: 'E7' }, ]); await expect(finalEntities(knex)).resolves.toEqual([ { entity_ref: 'E1', hash: 'original', next_stitch_at: null }, diff --git a/plugins/catalog-backend/src/database/tables.ts b/plugins/catalog-backend/src/database/tables.ts index 33e6250413..43068c8df8 100644 --- a/plugins/catalog-backend/src/database/tables.ts +++ b/plugins/catalog-backend/src/database/tables.ts @@ -81,47 +81,6 @@ export type DbRefreshStateRow = { * continuously gets moved forward as items are picked up for processing. */ next_update_at: string | Date; - /** - * If a stitch has been requested, this is the point in time that that last - * happened. - * - * @remarks - * - * Each time that a request is made, this timestamp is updated to the current - * time, overwriting the previous value if applicable. - * - * When the stitch loop runs and picks up an entity, this timestamp is not - * immediately reset. It's instead moved forward in time by a certain amount, - * which means that if the stitcher for some reason fails (eg if the process - * crashes or gets shut down), the entity will be picked up again in the - * future. - * - * Only when a stitch run is completed successfully, AND it's found that the - * stitch ticket has not changed since the start (which means that no new - * request has been made behind our backs), does the timestamp (and the - * ticket) get reset. - */ - next_stitch_at?: string | Date | null; - /** - * If a stitch has been requested, this is the unique ticket that was chosen - * to mark the last request. - * - * @remarks - * - * Each time that a request is made, a new random ticket is chosen, - * overwriting the previous value if applicable. - * - * When the stitch loop runs and picks up an entity, this column is left - * unchanged. This means that if the stitcher for some reason fails (eg if the - * process crashes or gets shut down), the entity will be picked up again in - * the future. - * - * Only when a stitch run is completed successfully, AND it's found that the - * stitch ticket has not changed since the start (which means that no new - * request has been made behind our backs), does the ticket (and the - * timestamp) get reset. - */ - next_stitch_ticket?: string | null; /** * The last time that this entity was emitted by somebody (the entity provider * or a parent entity). @@ -176,12 +135,52 @@ export type DbRelationsRow = { export type DbFinalEntitiesRow = { entity_id: string; hash: string; - stitch_ticket: string; final_entity?: string; last_updated_at?: string | Date; entity_ref: string; }; +/** + * Represents the stitch_queue table. + * + * @remarks + * + * Each row represents a pending stitch request for an entity. When a stitch + * completes successfully and the ticket hasn't changed, the row is deleted. + */ +export type DbStitchQueueRow = { + /** + * The entity ref that needs stitching (primary key). + */ + entity_ref: string; + /** + * A random value that changes with every new stitch request. Used for + * optimistic concurrency: when a stitch completes, the row is only deleted + * if this ticket still matches (meaning no new request came in while + * stitching was in progress). + */ + stitch_ticket: string; + /** + * The point in time when this entity should next be stitched. + * + * @remarks + * + * Each time that a request is made, this timestamp is updated to the current + * time, overwriting the previous value if applicable. + * + * When the stitch loop runs and picks up an entity, this timestamp is not + * immediately reset. It's instead moved forward in time by a certain amount, + * which means that if the stitcher for some reason fails (eg if the process + * crashes or gets shut down), the entity will be picked up again in the + * future. + * + * Only when a stitch run is completed successfully, AND it's found that the + * stitch ticket has not changed since the start (which means that no new + * request has been made behind our backs), does the row get deleted. + */ + next_stitch_at: string | Date; +}; + export type DbSearchRow = { entity_id: string; key: string; diff --git a/plugins/catalog-backend/src/database/util.test.ts b/plugins/catalog-backend/src/database/util.test.ts new file mode 100644 index 0000000000..b3f11b7f92 --- /dev/null +++ b/plugins/catalog-backend/src/database/util.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2026 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 { Knex } from 'knex'; +import { retryOnDeadlock } from './util'; + +jest.mock('node:timers/promises', () => ({ + setTimeout: jest.fn(), +})); + +function mockKnex(client: string): Knex { + return { client: { config: { client } } } as unknown as Knex; +} + +function pgDeadlockError(): Error & { code: string } { + const err = new Error('deadlock detected') as Error & { code: string }; + err.code = '40P01'; + return err; +} + +describe('retryOnDeadlock', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns the result on success', async () => { + const fn = jest.fn().mockResolvedValue('ok'); + const result = await retryOnDeadlock(fn, mockKnex('pg'), 3, 1); + expect(result).toBe('ok'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('retries on PostgreSQL deadlock errors', async () => { + const fn = jest + .fn() + .mockRejectedValueOnce(pgDeadlockError()) + .mockRejectedValueOnce(pgDeadlockError()) + .mockResolvedValue('recovered'); + + const result = await retryOnDeadlock(fn, mockKnex('pg'), 3, 1); + expect(result).toBe('recovered'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + it('throws after exhausting all retries', async () => { + const fn = jest.fn().mockRejectedValue(pgDeadlockError()); + + await expect(retryOnDeadlock(fn, mockKnex('pg'), 3, 1)).rejects.toThrow( + 'deadlock detected', + ); + // 1 initial + 3 retries = 4 calls + expect(fn).toHaveBeenCalledTimes(4); + }); + + it('does not retry non-deadlock errors on PostgreSQL', async () => { + const err = new Error('something else'); + const fn = jest.fn().mockRejectedValue(err); + + await expect(retryOnDeadlock(fn, mockKnex('pg'), 3, 1)).rejects.toThrow( + 'something else', + ); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('does not retry deadlock-like errors on non-PostgreSQL engines', async () => { + const fn = jest.fn().mockRejectedValue(pgDeadlockError()); + + await expect( + retryOnDeadlock(fn, mockKnex('better-sqlite3'), 3, 1), + ).rejects.toThrow('deadlock detected'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('applies exponential backoff between retries', async () => { + const { setTimeout: sleep } = jest.requireMock<{ + setTimeout: jest.Mock; + }>('node:timers/promises'); + + const fnCallsAtSleep: number[] = []; + const fn = jest + .fn() + .mockRejectedValueOnce(pgDeadlockError()) + .mockRejectedValueOnce(pgDeadlockError()) + .mockRejectedValueOnce(pgDeadlockError()) + .mockResolvedValue('done'); + + sleep.mockImplementation(async () => { + fnCallsAtSleep.push(fn.mock.calls.length); + }); + + const baseMs = 50; + const result = await retryOnDeadlock(fn, mockKnex('pg'), 3, baseMs); + + expect(result).toBe('done'); + expect(fn).toHaveBeenCalledTimes(4); + // Each sleep happens after fn has been called N times + expect(fnCallsAtSleep).toEqual([1, 2, 3]); + expect(sleep).toHaveBeenCalledTimes(3); + expect(sleep).toHaveBeenNthCalledWith(1, 50); + expect(sleep).toHaveBeenNthCalledWith(2, 100); + expect(sleep).toHaveBeenNthCalledWith(3, 200); + }); + + it('defaults to 3 retries when not specified', async () => { + const fn = jest.fn().mockRejectedValue(pgDeadlockError()); + + await expect(retryOnDeadlock(fn, mockKnex('pg'))).rejects.toThrow( + 'deadlock detected', + ); + expect(fn).toHaveBeenCalledTimes(4); + }); +}); diff --git a/plugins/catalog-backend/src/database/util.ts b/plugins/catalog-backend/src/database/util.ts index 3734670a41..39dc69c025 100644 --- a/plugins/catalog-backend/src/database/util.ts +++ b/plugins/catalog-backend/src/database/util.ts @@ -15,8 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { createHash } from 'node:crypto'; +import { ErrorLike, isError } from '@backstage/errors'; import stableStringify from 'fast-json-stable-stringify'; +import { Knex } from 'knex'; +import { createHash } from 'node:crypto'; +import { setTimeout as sleep } from 'node:timers/promises'; export function generateStableHash(entity: Entity) { return createHash('sha1') @@ -31,3 +34,43 @@ export function generateTargetKey(target: string) { .digest('hex')}` : target; } + +/** + * Retries an operation on database deadlock errors. + */ +export async function retryOnDeadlock( + fn: () => Promise, + knex: Knex | Knex.Transaction, + retries = 3, + baseMs = 25, +): Promise { + let attempt = 0; + for (;;) { + try { + return await fn(); + } catch (e: unknown) { + if (isDeadlockError(knex, e) && attempt < retries) { + await sleep(baseMs * Math.pow(2, attempt)); + attempt++; + continue; + } + throw e; + } + } +} + +/** + * Checks if the given error is a deadlock error for the database engine in use. + */ +function isDeadlockError( + knex: Knex | Knex.Transaction, + e: unknown, +): e is ErrorLike { + if (knex.client.config.client.includes('pg')) { + // PostgreSQL deadlock detection via error code + return isError(e) && e.code === '40P01'; + } + + // Add more database engine checks here as needed + return false; +} diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts index ae23830712..88829fc448 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.test.ts @@ -23,6 +23,7 @@ import { CatalogProcessingOrchestrator } from './types'; import { Stitcher } from '../stitching/types'; import { ConfigReader } from '@backstage/config'; import { mockServices } from '@backstage/backend-test-utils'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; describe('DefaultCatalogProcessingEngine', () => { const db = { @@ -72,6 +73,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, scheduler: mockServices.scheduler(), events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -141,6 +143,7 @@ describe('DefaultCatalogProcessingEngine', () => { scheduler: mockServices.scheduler(), createHash: () => hash, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -226,6 +229,7 @@ describe('DefaultCatalogProcessingEngine', () => { scheduler: mockServices.scheduler(), createHash: () => hash, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -305,6 +309,7 @@ describe('DefaultCatalogProcessingEngine', () => { scheduler: mockServices.scheduler(), createHash: () => hash, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -367,6 +372,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, pollingIntervalMs: 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -484,6 +490,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, pollingIntervalMs: 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -591,6 +598,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, pollingIntervalMs: 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -676,6 +684,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, pollingIntervalMs: 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -766,6 +775,7 @@ describe('DefaultCatalogProcessingEngine', () => { createHash: () => hash, pollingIntervalMs: 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); db.transaction.mockImplementation(cb => cb((() => {}) as any)); diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 3bcd4fcf44..9416fceea1 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -23,7 +23,7 @@ import { assertError, serializeError, stringifyError } from '@backstage/errors'; import { Hash } from 'node:crypto'; import stableStringify from 'fast-json-stable-stringify'; import { Knex } from 'knex'; -import { metrics, trace } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; import { ProcessingDatabase, RefreshStateItem } from '../database/types'; import { createCounterMetric, createSummaryMetric } from '../util/metrics'; import { CatalogProcessingOrchestrator, EntityProcessingResult } from './types'; @@ -39,6 +39,7 @@ import { deleteOrphanedEntities } from '../database/operations/util/deleteOrphan import { EventsService } from '@backstage/plugin-events-node'; import { CATALOG_ERRORS_TOPIC } from '../constants'; import { LoggerService, SchedulerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; const CACHE_TTL = 5; @@ -94,6 +95,7 @@ export class DefaultCatalogProcessingEngine { }) => Promise | void; tracker?: ProgressTracker; events: EventsService; + metrics: MetricsService; }) { this.config = options.config; this.scheduler = options.scheduler; @@ -106,7 +108,7 @@ export class DefaultCatalogProcessingEngine { this.pollingIntervalMs = options.pollingIntervalMs ?? 1_000; this.orphanCleanupIntervalMs = options.orphanCleanupIntervalMs ?? 30_000; this.onProcessingError = options.onProcessingError; - this.tracker = options.tracker ?? progressTracker(); + this.tracker = options.tracker ?? progressTracker(options.metrics); this.events = options.events; this.stopFunc = undefined; @@ -386,7 +388,7 @@ export class DefaultCatalogProcessingEngine { } // Helps wrap the timing and logging behaviors -function progressTracker() { +function progressTracker(metrics: MetricsService) { // prom-client metrics are deprecated in favour of OpenTelemetry metrics. const promProcessedEntities = createCounterMetric({ name: 'catalog_processed_entities_count', @@ -408,13 +410,12 @@ function progressTracker() { help: 'The amount of delay between being scheduled for processing, and the start of actually being processed, DEPRECATED, use OpenTelemetry metrics instead', }); - const meter = metrics.getMeter('default'); - const processedEntities = meter.createCounter( + const processedEntities = metrics.createCounter( 'catalog.processed.entities.count', { description: 'Amount of entities processed' }, ); - const processingDuration = meter.createHistogram( + const processingDuration = metrics.createHistogram( 'catalog.processing.duration', { description: 'Time spent executing the full processing flow', @@ -422,7 +423,7 @@ function progressTracker() { }, ); - const processorsDuration = meter.createHistogram( + const processorsDuration = metrics.createHistogram( 'catalog.processors.duration', { description: 'Time spent executing catalog processors', @@ -430,7 +431,7 @@ function progressTracker() { }, ); - const processingQueueDelay = meter.createHistogram( + const processingQueueDelay = metrics.createHistogram( 'catalog.processing.queue.delay', { description: diff --git a/plugins/catalog-backend/src/processing/TaskPipeline.test.ts b/plugins/catalog-backend/src/processing/TaskPipeline.test.ts index 43ec118bdb..02a7a2932c 100644 --- a/plugins/catalog-backend/src/processing/TaskPipeline.test.ts +++ b/plugins/catalog-backend/src/processing/TaskPipeline.test.ts @@ -121,8 +121,13 @@ describe('startTaskPipeline', () => { }); describe('createBarrier', () => { - const tick = (millis: number) => - new Promise(resolve => setTimeout(resolve, millis)); + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); it('abandons a wait after the timeout expires', async () => { const abortController = new AbortController(); @@ -132,10 +137,11 @@ describe('createBarrier', () => { const fn1 = jest.fn(); barrier.wait().then(fn1); - await tick(0); + await Promise.resolve(); expect(fn1).not.toHaveBeenCalled(); - await tick(50); + jest.advanceTimersByTime(50); + await Promise.resolve(); expect(fn1).not.toHaveBeenCalled(); // start a new wait mid-way through the timeout @@ -143,14 +149,16 @@ describe('createBarrier', () => { const fn2 = jest.fn(); barrier.wait().then(fn2); - await tick(0); + await Promise.resolve(); expect(fn2).not.toHaveBeenCalled(); - await tick(50); + jest.advanceTimersByTime(50); + await Promise.resolve(); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).not.toHaveBeenCalled(); - await tick(50); + jest.advanceTimersByTime(50); + await Promise.resolve(); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); }); @@ -164,16 +172,16 @@ describe('createBarrier', () => { barrier.wait().then(fn1); // should resolve immediately, not after timeout - await tick(0); + await Promise.resolve(); expect(fn1).not.toHaveBeenCalled(); abortController.abort(); - await tick(0); + await Promise.resolve(); expect(fn1).toHaveBeenCalledTimes(1); // subsequent waits should be immediate no matter what const fn2 = jest.fn(); barrier.wait().then(fn2); - await tick(0); + await Promise.resolve(); expect(fn2).toHaveBeenCalledTimes(1); }); @@ -185,7 +193,8 @@ describe('createBarrier', () => { const fn1 = jest.fn(); barrier.wait().then(fn1); - await tick(50); + jest.advanceTimersByTime(50); + await Promise.resolve(); expect(fn1).not.toHaveBeenCalled(); // start a new wait mid-way through the timeout @@ -193,13 +202,13 @@ describe('createBarrier', () => { const fn2 = jest.fn(); barrier.wait().then(fn2); - await tick(0); + await Promise.resolve(); expect(fn1).not.toHaveBeenCalled(); expect(fn2).not.toHaveBeenCalled(); barrier.release(); - await tick(0); + await Promise.resolve(); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); }); diff --git a/plugins/catalog-backend/src/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/processors/AnnotateScmSlugEntityProcessor.ts index a000f2f422..2cf9fcb24d 100644 --- a/plugins/catalog-backend/src/processors/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/processors/AnnotateScmSlugEntityProcessor.ts @@ -29,7 +29,10 @@ const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; const GITLAB_ACTIONS_ANNOTATION = 'gitlab.com/project-slug'; const AZURE_ACTIONS_ANNOTATION = 'dev.azure.com/project-repo'; -/** @public */ +/** + * @public + * @deprecated Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead, this will be removed in a future release + */ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { private readonly opts: { scmIntegrationRegistry: ScmIntegrationRegistry; diff --git a/plugins/catalog-backend/src/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/processors/CodeOwnersProcessor.ts index f6dc27101c..24dc2b9b54 100644 --- a/plugins/catalog-backend/src/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/processors/CodeOwnersProcessor.ts @@ -28,7 +28,10 @@ import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; const ALLOWED_LOCATION_TYPES = ['url']; -/** @public */ +/** + * @public + * @deprecated Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead, this will be removed in a future release + */ export class CodeOwnersProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: LoggerService; diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts index f12293fa1f..4f32c58816 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts @@ -15,7 +15,10 @@ */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; +import { + ANNOTATION_ORIGIN_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { v4 as uuid } from 'uuid'; import { applyDatabaseMigrations } from '../database/migrations'; import { @@ -25,6 +28,7 @@ import { DbSearchRow, } from '../database/tables'; import { DefaultLocationStore } from './DefaultLocationStore'; +import { locationSpecToLocationEntity } from '../util/conversion'; import { CatalogScmEventsServiceSubscriber } from '@backstage/plugin-catalog-node/alpha'; import waitFor from 'wait-for-expect'; @@ -35,6 +39,7 @@ describe('DefaultLocationStore', () => { const mockScmEvents = { subscribe: jest.fn(), publish: jest.fn(), + markEventActionTaken: jest.fn(), }; let subscriber: CatalogScmEventsServiceSubscriber | undefined; @@ -153,6 +158,48 @@ describe('DefaultLocationStore', () => { }); }, ); + + it.each(databases.eachSupportedId())( + 'updates refresh_state when onConflict is refresh, %p', + async databaseId => { + const { store, knex } = await createLocationStore(databaseId); + const spec = { + type: 'url', + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + }; + + // Create the location initially + await store.createLocation(spec); + + // Seed a refresh_state row for the corresponding Location entity + const entity = locationSpecToLocationEntity({ location: spec }); + const entityRef = stringifyEntityRef(entity); + const entityId = uuid(); + const oldDate = new Date('2020-01-01T00:00:00Z'); + await knex('refresh_state').insert({ + entity_id: entityId, + entity_ref: entityRef, + unprocessed_entity: '{}', + errors: '[]', + next_update_at: oldDate, + last_discovery_at: oldDate, + result_hash: 'old-hash', + }); + + // Re-register the same location with onConflict: 'refresh' + await store.createLocation(spec, { onConflict: 'refresh' }); + + // Verify that the refresh_state row was updated + const [row] = await knex('refresh_state').where({ + entity_ref: entityRef, + }); + expect(row.result_hash).toBe(''); + expect(new Date(row.next_update_at).getTime()).toBeGreaterThan( + oldDate.getTime(), + ); + }, + ); }); describe('deleteLocation', () => { @@ -224,7 +271,6 @@ describe('DefaultLocationStore', () => { final_entity: '{}', hash: 'hash', last_updated_at: new Date(), - stitch_ticket: '', entity_ref: 'k:ns/n', }); @@ -362,6 +408,11 @@ describe('DefaultLocationStore', () => { }, ], }); + + expect(mockScmEvents.markEventActionTaken).toHaveBeenCalledWith({ + count: 1, + action: 'delete', + }); }); }); @@ -483,6 +534,15 @@ describe('DefaultLocationStore', () => { ], removed: [], }); + + expect(mockScmEvents.markEventActionTaken).toHaveBeenCalledWith({ + count: 1, + action: 'delete', + }); + expect(mockScmEvents.markEventActionTaken).toHaveBeenCalledWith({ + count: 1, + action: 'create', + }); }); }); @@ -589,6 +649,11 @@ describe('DefaultLocationStore', () => { }, ], }); + + expect(mockScmEvents.markEventActionTaken).toHaveBeenCalledWith({ + count: 1, + action: 'delete', + }); }); }); @@ -709,6 +774,11 @@ describe('DefaultLocationStore', () => { ], removed: [], }); + + expect(mockScmEvents.markEventActionTaken).toHaveBeenCalledWith({ + count: 1, + action: 'move', + }); }); }); }); diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts index 305bd611dc..8619d13459 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts @@ -68,16 +68,27 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return 'DefaultLocationStore'; } - async createLocation(input: LocationInput): Promise { + async createLocation( + input: LocationInput, + options?: { + onConflict?: 'refresh' | 'reject'; + }, + ): Promise { + let existed = false; + const location = await this.db.transaction(async tx => { // Attempt to find a previous location matching the input const previousLocations = await this.locations(tx); // TODO: when location id's are a compilation of input target we can remove this full // lookup of locations first and just grab the by that instead. - const previousLocation = previousLocations.some( + const previousLocation = previousLocations.find( l => input.type === l.type && input.target === l.target, ); if (previousLocation) { + if (options?.onConflict === 'refresh') { + existed = true; + return previousLocation; + } throw new ConflictError( `Location ${input.type}:${input.target} already exists`, ); @@ -93,6 +104,9 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return inner; }); + + // Always upsert the entity, even if the location already existed, to + // recover from cases where the entity was inadvertently deleted. const entity = locationSpecToLocationEntity({ location }); await this.connection.applyMutation({ type: 'delta', @@ -100,6 +114,18 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { removed: [], }); + if (existed) { + // This is the "onConflict refresh" case, where a re-registration safely + // tries to recover from a bad state. + const entityRef = stringifyEntityRef(entity); + await this.db('refresh_state') + .where({ entity_ref: entityRef }) + .update({ + next_update_at: this.db.fn.now(), + result_hash: '', + }); + } + return location; } @@ -305,16 +331,40 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { } if (exactLocationsToDelete.size > 0) { - await this.#deleteLocationsByExactUrl(exactLocationsToDelete); + const count = await this.#deleteLocationsByExactUrl( + exactLocationsToDelete, + ); + this.scmEvents.markEventActionTaken({ + count, + action: 'delete', + }); } if (locationPrefixesToDelete.size > 0) { - await this.#deleteLocationsByUrlPrefix(locationPrefixesToDelete); + const count = await this.#deleteLocationsByUrlPrefix( + locationPrefixesToDelete, + ); + this.scmEvents.markEventActionTaken({ + count, + action: 'delete', + }); } if (exactLocationsToCreate.size > 0) { - await this.#createLocationsByExactUrl(exactLocationsToCreate); + const count = await this.#createLocationsByExactUrl( + exactLocationsToCreate, + ); + this.scmEvents.markEventActionTaken({ + count, + action: 'create', + }); } if (locationPrefixesToMove.size > 0) { - await this.#moveLocationsByUrlPrefix(locationPrefixesToMove); + const count = await this.#moveLocationsByUrlPrefix( + locationPrefixesToMove, + ); + this.scmEvents.markEventActionTaken({ + count, + action: 'move', + }); } } diff --git a/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.test.ts b/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.test.ts index bef8d1555d..6e20226677 100644 --- a/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.test.ts +++ b/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.test.ts @@ -23,6 +23,7 @@ import { CatalogScmEventsServiceSubscriber } from '@backstage/plugin-catalog-nod import { Knex } from 'knex'; import { applyDatabaseMigrations } from '../database/migrations'; import { + DbFinalEntitiesRow, DbRefreshKeysRow, DbRefreshStateRow, DbSearchRow, @@ -49,6 +50,7 @@ describe('GenericScmEventRefreshProvider', () => { return { unsubscribe: () => {} }; }), publish: jest.fn(), + markEventActionTaken: jest.fn(), }; const store = new GenericScmEventRefreshProvider(knex, scmEvents, { @@ -79,6 +81,15 @@ describe('GenericScmEventRefreshProvider', () => { }); } + async function insertFinalEntity(knex: Knex, id: string) { + await knex('final_entities').insert({ + entity_id: id, + entity_ref: `k:ns/${id}`, + hash: 'h', + final_entity: '{}', + }); + } + describe.each(databases.eachSupportedId())('%p', databaseId => { it('handles location.updated', async () => { const { knex, subscriber } = await initialize(databaseId); @@ -109,6 +120,11 @@ describe('GenericScmEventRefreshProvider', () => { }, ]); + // Insert final_entities for entities that will have search rows + await insertFinalEntity(knex, '4'); + await insertFinalEntity(knex, '5'); + await insertFinalEntity(knex, '6'); + await knex('search').insert([ // match exact blob in location { @@ -188,6 +204,13 @@ describe('GenericScmEventRefreshProvider', () => { }, ]); + // Insert final_entities for entities that will have search rows + await insertFinalEntity(knex, '4'); + await insertFinalEntity(knex, '5'); + await insertFinalEntity(knex, '6'); + await insertFinalEntity(knex, '7'); + await insertFinalEntity(knex, '8'); + await knex('search').insert([ // match blob in location { diff --git a/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.ts b/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.ts index 9d62ef1bdf..31cb6eb62b 100644 --- a/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.ts +++ b/plugins/catalog-backend/src/providers/GenericScmEventRefreshProvider.ts @@ -150,6 +150,8 @@ export class GenericScmEventRefreshProvider implements EntityProvider { count += Number(result); } + + this.#scmEvents.markEventActionTaken({ count, action: 'refresh' }); } } diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index 9f78cc6210..d0266e4d46 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: catalog version: '1' @@ -25,6 +25,9 @@ info: name: Apache-2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html contact: {} +tags: + - name: Entity + - name: Locations servers: - url: / components: @@ -35,28 +38,24 @@ components: name: kind in: path required: true - allowReserved: true schema: type: string namespace: name: namespace in: path required: true - allowReserved: true schema: type: string name: name: name in: path required: true - allowReserved: true schema: type: string uid: name: uid in: path required: true - allowReserved: true schema: type: string cursor: @@ -478,31 +477,32 @@ components: - apiVersion description: The parts of the format that's common to all versions/kinds of entity. NullableEntity: - type: object - properties: - relations: - type: array - items: - $ref: '#/components/schemas/EntityRelation' - description: The relations that this entity has with other entities. - spec: - $ref: '#/components/schemas/JsonObject' - metadata: - $ref: '#/components/schemas/EntityMeta' - kind: - type: string - description: The high level entity type being described. - apiVersion: - type: string - description: |- - The version of specification format for this particular entity that - this is written against. - required: - - metadata - - kind - - apiVersion - description: The parts of the format that's common to all versions/kinds of entity. - nullable: true + anyOf: + - type: object + properties: + relations: + type: array + items: + $ref: '#/components/schemas/EntityRelation' + description: The relations that this entity has with other entities. + spec: + $ref: '#/components/schemas/JsonObject' + metadata: + $ref: '#/components/schemas/EntityMeta' + kind: + type: string + description: The high level entity type being described. + apiVersion: + type: string + description: |- + The version of specification format for this particular entity that + this is written against. + required: + - metadata + - kind + - apiVersion + description: The parts of the format that's common to all versions/kinds of entity. + - type: 'null' EntityAncestryResponse: type: object properties: @@ -748,8 +748,9 @@ components: field empty; which would currently make it owned by X" where X is taken from the codeowners file. value: - type: string - nullable: true + oneOf: + - type: string + - type: 'null' state: type: string enum: @@ -1036,6 +1037,8 @@ paths: type: array items: type: string + query: + $ref: '#/components/schemas/JsonObject' examples: Fetch Backstage entities: value: @@ -1095,6 +1098,68 @@ paths: type: string explode: false style: form + post: + operationId: QueryEntitiesByPredicate + tags: + - Entity + description: Query entities using predicate-based filters. + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntitiesQueryResponse' + '400': + $ref: '#/components/responses/ErrorResponse' + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + cursor: + type: string + limit: + type: number + offset: + type: number + orderBy: + type: array + items: + type: object + required: + - field + - order + properties: + field: + type: string + order: + type: string + enum: + - asc + - desc + fullTextFilter: + type: object + properties: + term: + type: string + fields: + type: array + items: + type: string + fields: + type: array + items: + type: string + query: + $ref: '#/components/schemas/JsonObject' /entity-facets: get: operationId: GetEntityFacets @@ -1132,6 +1197,40 @@ paths: value: - spec.type - $ref: '#/components/parameters/filter' + post: + operationId: QueryEntityFacetsByPredicate + tags: + - Entity + description: Get entity facets using predicate-based filters. + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntityFacetsResponse' + '400': + $ref: '#/components/responses/ErrorResponse' + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - facets + properties: + facets: + type: array + items: + type: string + query: + $ref: '#/components/schemas/JsonObject' /locations: post: operationId: CreateLocation @@ -1171,6 +1270,19 @@ paths: allowReserved: true schema: type: string + - in: query + name: onConflict + required: false + allowReserved: true + schema: + type: string + enum: + - refresh + - reject + description: >- + Behavior when the location already exists. 'reject' (default) + returns a 409 error, 'refresh' triggers a refresh of the + existing location entity and returns 201. requestBody: required: true content: @@ -1263,7 +1375,6 @@ paths: - in: path name: id required: true - allowReserved: true schema: type: string delete: @@ -1285,7 +1396,6 @@ paths: - in: path name: id required: true - allowReserved: true schema: type: string /locations/by-entity/{kind}/{namespace}/{name}: @@ -1310,19 +1420,16 @@ paths: - in: path name: kind required: true - allowReserved: true schema: type: string - in: path name: namespace required: true - allowReserved: true schema: type: string - in: path name: name required: true - allowReserved: true schema: type: string /analyze-location: diff --git a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts index 8254df6bab..81a952a166 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -19,13 +19,14 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntitiesBatchResponse } from '../models/EntitiesBatchResponse.model'; import { EntitiesQueryResponse } from '../models/EntitiesQueryResponse.model'; import { Entity } from '../models/Entity.model'; import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model'; import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model'; import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model'; +import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model'; +import { QueryEntityFacetsByPredicateRequest } from '../models/QueryEntityFacetsByPredicateRequest.model'; import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model'; import { ValidateEntity400Response } from '../models/ValidateEntity400Response.model'; import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model'; @@ -120,6 +121,20 @@ export type GetEntityFacets = { }; response: EntityFacetsResponse | Error | Error; }; +/** + * @public + */ +export type QueryEntitiesByPredicate = { + body: QueryEntitiesByPredicateRequest; + response: EntitiesQueryResponse | Error | Error; +}; +/** + * @public + */ +export type QueryEntityFacetsByPredicate = { + body: QueryEntityFacetsByPredicateRequest; + response: EntityFacetsResponse | Error | Error; +}; /** * @public */ @@ -157,6 +172,7 @@ export type CreateLocation = { body: CreateLocationRequest; query: { dryRun?: string; + onConflict?: 'refresh' | 'reject'; }; response: CreateLocation201Response | Error | Error; }; @@ -220,6 +236,10 @@ export type EndpointMap = { '#get|/entity-facets': GetEntityFacets; + '#post|/entities/by-query': QueryEntitiesByPredicate; + + '#post|/entity-facets': QueryEntityFacetsByPredicate; + '#post|/refresh': RefreshEntity; '#post|/validate-entity': ValidateEntity; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index ea3d097ec5..06998f5422 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { LocationSpec } from '../models/LocationSpec.model'; /** - * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren't already + * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren\'t already * @public */ export interface AnalyzeLocationExistingEntity { diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index 2999da3ddf..7b2ba72114 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationEntityField } from '../models/AnalyzeLocationEntityField.model'; import { RecursivePartialEntity } from '../models/RecursivePartialEntity.model'; /** - * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It'll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. + * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It\'ll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. * @public */ export interface AnalyzeLocationGenerateEntity { diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index 5099a5c8aa..a0a5615f25 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { LocationInput } from '../models/LocationInput.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index a5eac33f7c..d4d8a0fdd9 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationExistingEntity } from '../models/AnalyzeLocationExistingEntity.model'; import { AnalyzeLocationGenerateEntity } from '../models/AnalyzeLocationGenerateEntity.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 610e565b3e..e224386de4 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { Location } from '../models/Location.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index e8bf79a501..0434965888 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { NullableEntity } from '../models/NullableEntity.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index d201466db6..5165faec58 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntitiesQueryResponsePageInfo } from '../models/EntitiesQueryResponsePageInfo.model'; import { Entity } from '../models/Entity.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts index 108a7b15b8..00cc91e89b 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export interface Entity { diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index f2c1116f35..9d1e4d9052 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityAncestryResponseItemsInner } from '../models/EntityAncestryResponseItemsInner.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 9c816e5517..0936545ec7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index 5cc67294e5..540650bcaf 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityFacet } from '../models/EntityFacet.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts index c1e1471045..a35ee87b61 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** @@ -26,7 +25,6 @@ import { EntityLink } from '../models/EntityLink.model'; */ export interface EntityMeta { [key: string]: any; - /** * A list of external hyperlinks related to the entity. */ diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 0e7255f9c6..607c992ace 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -24,4 +24,8 @@ export interface GetEntitiesByRefsRequest { entityRefs: Array; fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index b77b78971f..846b58ba97 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts index c231fccc57..05259caf32 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; import { LocationsQueryResponsePageInfo } from '../models/LocationsQueryResponsePageInfo.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts index 207f16f828..9d5c36325e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ErrorError } from '../models/ErrorError.model'; import { ErrorRequest } from '../models/ErrorRequest.model'; import { ErrorResponse } from '../models/ErrorResponse.model'; @@ -27,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts index 5e59af3327..288b6febb6 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export type NullableEntity = { diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts similarity index 54% rename from plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts rename to plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts index 5b6c2ff9c7..7196156f55 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,13 +17,21 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { DryRun200ResponseAllOfDirectoryContentsInner } from '../models/DryRun200ResponseAllOfDirectoryContentsInner.model'; -import { DryRun200ResponseAllOfStepsInner } from '../models/DryRun200ResponseAllOfStepsInner.model'; +import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; /** * @public */ -export interface DryRun200ResponseAllOf { - steps: Array; - directoryContents?: Array; +export interface QueryEntitiesByPredicateRequest { + cursor?: string; + limit?: number; + offset?: number; + orderBy?: Array; + fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter; + fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts similarity index 84% rename from plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts rename to plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts index 0b33852cc9..e7e971e1ea 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,6 +21,7 @@ /** * @public */ -export interface TaskSecretsAllOf { - backstageToken?: string; +export interface QueryEntitiesByPredicateRequestFullTextFilter { + term?: string; + fields?: Array; } diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts new file mode 100644 index 0000000000..373ca9715f --- /dev/null +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface QueryEntitiesByPredicateRequestOrderByInner { + field: string; + order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum; +} + +/** + * @public + */ +export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum = + | 'asc' + | 'desc'; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts similarity index 78% rename from plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts rename to plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts index e67240ee54..14fd3a8bc0 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,13 +17,14 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { TaskStatus } from '../models/TaskStatus.model'; /** * @public */ -export interface DryRunResultLogInnerBodyAllOf { - message: string; - status?: TaskStatus; - stepId?: string; +export interface QueryEntityFacetsByPredicateRequest { + facets: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index 3f09c69672..3a607dd878 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { RecursivePartialEntityMeta } from '../models/RecursivePartialEntityMeta.model'; import { RecursivePartialEntityRelation } from '../models/RecursivePartialEntityRelation.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index 2db84dd085..7c2d3bed48 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts deleted file mode 100644 index caca69a622..0000000000 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2026 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. - */ - -// ****************************************************************** -// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * -// ****************************************************************** - -import { EntityLink } from '../models/EntityLink.model'; - -/** - * Metadata fields common to all versions/kinds of entity. - * @public - */ -export interface RecursivePartialEntityMetaAllOf { - /** - * A list of external hyperlinks related to the entity. - */ - links?: Array; - /** - * A list of single-valued strings, to for example classify catalog entities in various ways. - */ - tags?: Array; - /** - * Construct a type with a set of properties K of type T - */ - annotations?: { [key: string]: string }; - /** - * Construct a type with a set of properties K of type T - */ - labels?: { [key: string]: string }; - /** - * A short (typically relatively few words, on one line) description of the entity. - */ - description?: string; - /** - * A display name of the entity, to be presented in user interfaces instead of the `name` property above, when available. This field is sometimes useful when the `name` is cumbersome or ends up being perceived as overly technical. The title generally does not have as stringent format requirements on it, so it may contain special characters and be more explanatory. Do keep it very short though, and avoid situations where a title can be confused with the name of another entity, or where two entities share a title. Note that this is only for display purposes, and may be ignored by some parts of the code. Entity references still always make use of the `name` property, not the title. - */ - title?: string; - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - /** - * The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair. This value is part of the technical identifier of the entity, and as such it will appear in URLs, database tables, entity references, and similar. It is subject to restrictions regarding what characters are allowed. If you want to use a different, more human readable string with fewer restrictions on it in user interfaces, see the `title` field below. - */ - name?: string; - /** - * An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value. - */ - etag?: string; - /** - * A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics. - */ - uid?: string; -} diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index aabf825a57..2a41f2df5c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ValidateEntity400ResponseErrorsInner } from '../models/ValidateEntity400ResponseErrorsInner.model'; /** diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 4582bf2046..f8a7ae4386 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -23,7 +23,6 @@ */ export interface ValidateEntity400ResponseErrorsInner { [key: string]: any; - name: string; message: string; } diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts index abdb58378c..aaafc8e60e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts @@ -45,9 +45,12 @@ export * from '../models/LocationsQueryResponse.model'; export * from '../models/LocationsQueryResponsePageInfo.model'; export * from '../models/ModelError.model'; export * from '../models/NullableEntity.model'; +export * from '../models/QueryEntitiesByPredicateRequest.model'; +export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; +export * from '../models/QueryEntityFacetsByPredicateRequest.model'; export * from '../models/RecursivePartialEntity.model'; export * from '../models/RecursivePartialEntityMeta.model'; -export * from '../models/RecursivePartialEntityMetaAllOf.model'; export * from '../models/RecursivePartialEntityRelation.model'; export * from '../models/RefreshEntityRequest.model'; export * from '../models/ValidateEntity400Response.model'; diff --git a/plugins/catalog-backend/src/schema/openapi/generated/router.ts b/plugins/catalog-backend/src/schema/openapi/generated/router.ts index b4078df721..6b92c7ae63 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/router.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/router.ts @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: 'catalog', version: '1', @@ -33,6 +33,14 @@ export const spec = { }, contact: {}, }, + tags: [ + { + name: 'Entity', + }, + { + name: 'Locations', + }, + ], servers: [ { url: '/', @@ -46,7 +54,6 @@ export const spec = { name: 'kind', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -55,7 +62,6 @@ export const spec = { name: 'namespace', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -64,7 +70,6 @@ export const spec = { name: 'name', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -73,7 +78,6 @@ export const spec = { name: 'uid', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -404,36 +408,42 @@ export const spec = { "The parts of the format that's common to all versions/kinds of entity.", }, NullableEntity: { - type: 'object', - properties: { - relations: { - type: 'array', - items: { - $ref: '#/components/schemas/EntityRelation', + anyOf: [ + { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + $ref: '#/components/schemas/EntityRelation', + }, + description: + 'The relations that this entity has with other entities.', + }, + spec: { + $ref: '#/components/schemas/JsonObject', + }, + metadata: { + $ref: '#/components/schemas/EntityMeta', + }, + kind: { + type: 'string', + description: 'The high level entity type being described.', + }, + apiVersion: { + type: 'string', + description: + 'The version of specification format for this particular entity that\nthis is written against.', + }, }, + required: ['metadata', 'kind', 'apiVersion'], description: - 'The relations that this entity has with other entities.', + "The parts of the format that's common to all versions/kinds of entity.", }, - spec: { - $ref: '#/components/schemas/JsonObject', + { + type: 'null', }, - metadata: { - $ref: '#/components/schemas/EntityMeta', - }, - kind: { - type: 'string', - description: 'The high level entity type being described.', - }, - apiVersion: { - type: 'string', - description: - 'The version of specification format for this particular entity that\nthis is written against.', - }, - }, - required: ['metadata', 'kind', 'apiVersion'], - description: - "The parts of the format that's common to all versions/kinds of entity.", - nullable: true, + ], }, EntityAncestryResponse: { type: 'object', @@ -703,8 +713,14 @@ export const spec = { 'A text to show to the user to inform about the choices made. Like, it could say\n"Found a CODEOWNERS file that covers this target, so we suggest leaving this\nfield empty; which would currently make it owned by X" where X is taken from the\ncodeowners file.', }, value: { - type: 'string', - nullable: true, + oneOf: [ + { + type: 'string', + }, + { + type: 'null', + }, + ], }, state: { type: 'string', @@ -1124,6 +1140,9 @@ export const spec = { type: 'string', }, }, + query: { + $ref: '#/components/schemas/JsonObject', + }, }, }, examples: { @@ -1228,6 +1247,95 @@ export const spec = { }, ], }, + post: { + operationId: 'QueryEntitiesByPredicate', + tags: ['Entity'], + description: 'Query entities using predicate-based filters.', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntitiesQueryResponse', + }, + }, + }, + }, + '400': { + $ref: '#/components/responses/ErrorResponse', + }, + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + requestBody: { + required: false, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + cursor: { + type: 'string', + }, + limit: { + type: 'number', + }, + offset: { + type: 'number', + }, + orderBy: { + type: 'array', + items: { + type: 'object', + required: ['field', 'order'], + properties: { + field: { + type: 'string', + }, + order: { + type: 'string', + enum: ['asc', 'desc'], + }, + }, + }, + }, + fullTextFilter: { + type: 'object', + properties: { + term: { + type: 'string', + }, + fields: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + fields: { + type: 'array', + items: { + type: 'string', + }, + }, + query: { + $ref: '#/components/schemas/JsonObject', + }, + }, + }, + }, + }, + }, + }, }, '/entity-facets': { get: { @@ -1284,6 +1392,57 @@ export const spec = { }, ], }, + post: { + operationId: 'QueryEntityFacetsByPredicate', + tags: ['Entity'], + description: 'Get entity facets using predicate-based filters.', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntityFacetsResponse', + }, + }, + }, + }, + '400': { + $ref: '#/components/responses/ErrorResponse', + }, + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['facets'], + properties: { + facets: { + type: 'array', + items: { + type: 'string', + }, + }, + query: { + $ref: '#/components/schemas/JsonObject', + }, + }, + }, + }, + }, + }, + }, }, '/locations': { post: { @@ -1339,6 +1498,18 @@ export const spec = { type: 'string', }, }, + { + in: 'query', + name: 'onConflict', + required: false, + allowReserved: true, + schema: { + type: 'string', + enum: ['refresh', 'reject'], + }, + description: + "Behavior when the location already exists. 'reject' (default) returns a 409 error, 'refresh' triggers a refresh of the existing location entity and returns 201.", + }, ], requestBody: { required: true, @@ -1477,7 +1648,6 @@ export const spec = { in: 'path', name: 'id', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -1510,7 +1680,6 @@ export const spec = { in: 'path', name: 'id', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -1549,7 +1718,6 @@ export const spec = { in: 'path', name: 'kind', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -1558,7 +1726,6 @@ export const spec = { in: 'path', name: 'namespace', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -1567,7 +1734,6 @@ export const spec = { in: 'path', name: 'name', required: true, - allowReserved: true, schema: { type: 'string', }, diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 894949a691..85fbd16eeb 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -23,6 +23,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; import { Cursor, QueryEntitiesResponse } from '../catalog/types'; import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '@backstage/plugin-catalog-node'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { mockCredentials } from '@backstage/backend-test-utils'; describe('AuthorizedEntitiesCatalog', () => { @@ -306,6 +307,144 @@ describe('AuthorizedEntitiesCatalog', () => { }, }); }); + + it('passes through query alongside permission filter on CONDITIONAL with initial request', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['b'] }, + }, + }, + ]); + + const userQuery: FilterPredicate = { 'metadata.name': 'my-entity' }; + + const entities = [ + { + kind: 'component', + namespace: 'default', + name: 'a', + } as unknown as Entity, + ]; + + fakeCatalog.queryEntities.mockResolvedValue({ + items: { type: 'object', entities }, + pageInfo: { + nextCursor: { + isPrevious: false, + orderFieldValues: ['xxx', null], + query: userQuery, + filter: { key: 'kind', values: ['b'] }, + orderFields: [{ field: 'name', order: 'asc' }], + }, + }, + totalItems: 1, + } as QueryEntitiesResponse); + + const catalog = createCatalog(isEntityKind); + + const response = await catalog.queryEntities({ + credentials: mockCredentials.none(), + query: userQuery, + }); + + expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ + credentials: mockCredentials.none(), + query: userQuery, + filter: { key: 'kind', values: ['b'] }, + }); + + expect(response.pageInfo.nextCursor).toEqual({ + isPrevious: false, + orderFieldValues: ['xxx', null], + query: userQuery, + filter: undefined, + orderFields: [{ field: 'name', order: 'asc' }], + }); + }); + + it('passes through cursor query alongside permission filter on CONDITIONAL with cursor request', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['b'] }, + }, + }, + ]); + + const userQuery: FilterPredicate = { 'metadata.name': 'my-entity' }; + + const entities = [ + { + kind: 'component', + namespace: 'default', + name: 'a', + } as unknown as Entity, + ]; + + fakeCatalog.queryEntities.mockResolvedValue({ + items: { type: 'object', entities }, + pageInfo: { + nextCursor: { + isPrevious: false, + orderFieldValues: ['yyy', null], + query: userQuery, + filter: { key: 'kind', values: ['b'] }, + orderFields: [{ field: 'name', order: 'asc' }], + }, + prevCursor: { + isPrevious: true, + orderFieldValues: ['aaa', null], + query: userQuery, + filter: { key: 'kind', values: ['b'] }, + orderFields: [{ field: 'name', order: 'asc' }], + }, + }, + totalItems: 3, + } as QueryEntitiesResponse); + + const catalog = createCatalog(isEntityKind); + + const cursor: Cursor = { + query: userQuery, + orderFields: [{ field: 'name', order: 'asc' }], + isPrevious: false, + orderFieldValues: ['xxx', null], + }; + + const response = await catalog.queryEntities({ + credentials: mockCredentials.none(), + cursor, + }); + + expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({ + credentials: mockCredentials.none(), + cursor: { + ...cursor, + filter: { key: 'kind', values: ['b'] }, + }, + }); + + expect(response.pageInfo.nextCursor).toEqual({ + isPrevious: false, + orderFieldValues: ['yyy', null], + query: userQuery, + filter: undefined, + orderFields: [{ field: 'name', order: 'asc' }], + }); + + expect(response.pageInfo.prevCursor).toEqual({ + isPrevious: true, + orderFieldValues: ['aaa', null], + query: userQuery, + filter: undefined, + orderFields: [{ field: 'name', order: 'asc' }], + }); + }); }); describe('removeEntityByUid', () => { diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index d3d0dba5b3..910188b036 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -35,6 +35,7 @@ import { QueryEntitiesRequest, QueryEntitiesResponse, } from '../catalog/types'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { basicEntityFilter } from './request'; import { isQueryEntitiesCursorRequest } from './util'; import { EntityFilter } from '@backstage/plugin-catalog-node'; @@ -147,9 +148,11 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { let permissionedRequest: QueryEntitiesRequest; let requestFilter: EntityFilter | undefined; + let requestQuery: FilterPredicate | undefined; if (isQueryEntitiesCursorRequest(request)) { requestFilter = request.cursor.filter; + requestQuery = request.cursor.query; permissionedRequest = { ...request, @@ -161,13 +164,15 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { }, }; } else { + requestFilter = request.filter; + requestQuery = request.query; + permissionedRequest = { ...request, filter: request.filter ? { allOf: [permissionFilter, request.filter] } : permissionFilter, }; - requestFilter = request.filter; } const response = await this.entitiesCatalog.queryEntities( @@ -177,11 +182,13 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { const prevCursor: Cursor | undefined = response.pageInfo.prevCursor && { ...response.pageInfo.prevCursor, filter: requestFilter, + query: requestQuery, }; const nextCursor: Cursor | undefined = response.pageInfo.nextCursor && { ...response.pageInfo.nextCursor, filter: requestFilter, + query: requestQuery, }; return { @@ -193,6 +200,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { }; } + // The ALLOW case return this.entitiesCatalog.queryEntities(request); } diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 83f5118153..1b18d50324 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -46,6 +46,7 @@ export class AuthorizedLocationService implements LocationService { spec: LocationInput, dryRun: boolean, options: { + onConflict?: 'refresh' | 'reject'; credentials: BackstageCredentials; }, ): Promise<{ diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e3d0c26b0f..d3bf47874d 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -117,6 +117,7 @@ import { import { filterAndSortProcessors, filterProviders } from './util'; import { GenericScmEventRefreshProvider } from '../providers/GenericScmEventRefreshProvider'; import { readScmEventHandlingConfig } from '../util/readScmEventHandlingConfig'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; export type CatalogEnvironment = { logger: LoggerService; @@ -131,6 +132,7 @@ export type CatalogEnvironment = { auditor: AuditorService; events: EventsService; catalogScmEvents: CatalogScmEventsService; + metrics: MetricsService; }; /** @@ -429,6 +431,7 @@ export class CatalogBuilder { httpAuth, events, catalogScmEvents, + metrics, } = this.env; const enableRelationsCompatibility = Boolean( @@ -448,6 +451,7 @@ export class CatalogBuilder { const stitcher = DefaultStitcher.fromConfig(config, { knex: dbClient, logger, + metrics, }); const processingDatabase = new DefaultProcessingDatabase({ @@ -455,6 +459,7 @@ export class CatalogBuilder { logger, events, refreshInterval: this.processingInterval, + metrics, }); const providerDatabase = new DefaultProviderDatabase({ database: dbClient, @@ -577,6 +582,7 @@ export class CatalogBuilder { this.onProcessingError?.(event); }, events, + metrics, }); const locationAnalyzer = @@ -588,6 +594,10 @@ export class CatalogBuilder { const locationService = new AuthorizedLocationService( new DefaultLocationService(locationStore, orchestrator, { allowedLocationTypes: this.allowedLocationType, + defaultLocationConflictStrategy: + (config.getOptionalString( + 'catalog.defaultLocationConflictStrategy', + ) as 'refresh' | 'reject') || 'reject', }), permissionsService, ); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 26b09b9eea..915dd80dd9 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -45,7 +45,10 @@ import { eventsServiceRef } from '@backstage/plugin-events-node'; import { Permission } from '@backstage/plugin-permission-common'; import { merge } from 'lodash'; import { CatalogBuilder } from './CatalogBuilder'; -import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + actionsRegistryServiceRef, + metricsServiceRef, +} from '@backstage/backend-plugin-api/alpha'; import { createCatalogActions } from '../actions'; import type { EntityProviderEntry } from '../processing/connectEntityProviders'; @@ -219,6 +222,7 @@ export const catalogPlugin = createBackendPlugin({ catalog: catalogServiceRef, actionsRegistry: actionsRegistryServiceRef, catalogScmEvents: catalogScmEventsServiceRef, + metrics: metricsServiceRef, }, async init({ logger, @@ -237,6 +241,7 @@ export const catalogPlugin = createBackendPlugin({ auditor, events, catalogScmEvents, + metrics, }) { const builder = await CatalogBuilder.create({ config, @@ -251,6 +256,7 @@ export const catalogPlugin = createBackendPlugin({ auditor, events, catalogScmEvents, + metrics, }); if (onProcessingError) { @@ -301,6 +307,22 @@ export const catalogPlugin = createBackendPlugin({ catalog, actionsRegistry, }); + + const scmEventsMessagesCounter = metrics.createCounter<{ + eventType: string; + }>('catalog.events.scm.messages', { + description: + 'Number of SCM event messages received by the catalog backend', + unit: 'short', + }); + catalogScmEvents.subscribe({ + onEvents: async e => { + for (const event of e) { + const eventType = event.type.split('.')[0]; + scmEventsMessagesCounter.add(1, { eventType }); + } + }, + }); }, }); }, diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 7012c2e505..3a184676df 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -80,7 +80,6 @@ describe('DefaultEntitiesCatalog', () => { entity_ref: entityRef, final_entity: entityJson, hash: 'h', - stitch_ticket: '', }); for (const parent of parents) { @@ -118,7 +117,6 @@ describe('DefaultEntitiesCatalog', () => { entity_ref: entityRef, final_entity: entityJson, hash: 'h', - stitch_ticket: '', }); for (const row of buildEntitySearch(id, entity)) { @@ -2054,6 +2052,38 @@ describe('DefaultEntitiesCatalog', () => { ]); }, ); + + it.each(databases.eachSupportedId())( + 'should apply both filter and query when both are given, %p', + async databaseId => { + await createDatabase(databaseId); + + // Add entities with different kinds and names + await addEntityToSearch(entityFrom('A', { kind: 'component' })); + await addEntityToSearch(entityFrom('B', { kind: 'component' })); + await addEntityToSearch(entityFrom('C', { kind: 'api' })); + await addEntityToSearch(entityFrom('D', { kind: 'api' })); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + // Use filter to restrict to kind=component, and query to restrict to name=A + const response = await catalog.queryEntities({ + filter: { key: 'kind', values: ['component'] }, + query: { 'metadata.name': 'a' }, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + credentials: mockCredentials.none(), + }); + + const resultEntities = entitiesResponseToObjects(response.items); + expect(resultEntities).toEqual([ + entityFrom('A', { kind: 'component' }), + ]); + }, + ); }); describe('removeEntityByUid', () => { diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 92c0bb0ecf..7fda0ac0f1 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -18,7 +18,6 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { InputError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { chunk as lodashChunk, isEqual } from 'lodash'; -import { z } from 'zod'; import { Cursor, EntitiesBatchRequest, @@ -49,7 +48,6 @@ import { isQueryEntitiesCursorRequest, isQueryEntitiesInitialRequest, } from './util'; -import { EntityFilter } from '@backstage/plugin-catalog-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { applyEntityFilterToQuery } from './request/applyEntityFilterToQuery'; import { processRawEntitiesResult } from './response'; @@ -226,9 +224,10 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { }) .whereIn('final_entities.entity_ref', chunk); - if (request?.filter) { + if (request?.filter || request?.query) { query = applyEntityFilterToQuery({ filter: request.filter, + query: request.query, targetQuery: query, onEntityIdField: 'final_entities.entity_id', knex: this.database, @@ -303,10 +302,11 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { }); } - // Add regular filters, if given - if (cursor.filter) { + // Add regular filters and/or predicate query, if given + if (cursor.filter || cursor.query) { applyEntityFilterToQuery({ filter: cursor.filter, + query: cursor.query, targetQuery: inner, onEntityIdField: 'final_entities.entity_id', knex: this.database, @@ -525,94 +525,96 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } async removeEntityByUid(uid: string): Promise { - const dbConfig = this.database.client.config; + const relationPeerRefs = await this.database.transaction(async tx => { + const dbConfig = tx.client.config; - // Clear the hashed state of the immediate parents of the deleted entity. - // This makes sure that when they get reprocessed, their output is written - // down again. The reason for wanting to do this, is that if the user - // deletes entities that ARE still emitted by the parent, the parent - // processing will still generate the same output hash as always, which - // means it'll never try to write down the children again (it assumes that - // they already exist). This means that without the code below, the database - // never "heals" from accidental deletes. - if (dbConfig.client.includes('mysql')) { - // MySQL doesn't support the syntax we need to do this in a single query, - // http://dev.mysql.com/doc/refman/5.6/en/update.html - const results = await this.database('refresh_state') - .select('entity_id') - .whereIn('entity_ref', function parents(builder) { - return builder - .from('refresh_state') - .innerJoin( - 'refresh_state_references', - { - 'refresh_state_references.target_entity_ref': - 'refresh_state.entity_ref', - }, - ) - .where('refresh_state.entity_id', '=', uid) - .select('refresh_state_references.source_entity_ref'); - }); - await this.database('refresh_state') - .update({ - result_hash: 'child-was-deleted', - next_update_at: this.database.fn.now(), - }) - .whereIn( - 'entity_id', - results.map(key => key.entity_id), - ); - } else { - await this.database('refresh_state') - .update({ - result_hash: 'child-was-deleted', - next_update_at: this.database.fn.now(), - }) - .whereIn('entity_ref', function parents(builder) { - return builder - .from('refresh_state') - .innerJoin( - 'refresh_state_references', - { - 'refresh_state_references.target_entity_ref': - 'refresh_state.entity_ref', - }, - ) - .where('refresh_state.entity_id', '=', uid) - .select('refresh_state_references.source_entity_ref'); - }); - } - - // Stitch the entities that the deleted one had relations to. If we do not - // do this, the entities in the other end of the relations will still look - // like they have a relation to the entity that was deleted, despite not - // having any corresponding rows in the relations table. - const relationPeers = await this.database - .from('relations') - .innerJoin('refresh_state', { - 'refresh_state.entity_ref': 'relations.target_entity_ref', - }) - .where('relations.originating_entity_id', '=', uid) - .andWhere('refresh_state.entity_id', '!=', uid) - .select({ ref: 'relations.target_entity_ref' }) - .union(other => - other - .from('relations') - .innerJoin('refresh_state', { - 'refresh_state.entity_ref': 'relations.source_entity_ref', + // Clear the hashed state of the immediate parents of the deleted entity. + // This makes sure that when they get reprocessed, their output is written + // down again. The reason for wanting to do this, is that if the user + // deletes entities that ARE still emitted by the parent, the parent + // processing will still generate the same output hash as always, which + // means it'll never try to write down the children again (it assumes that + // they already exist). This means that without the code below, the database + // never "heals" from accidental deletes. + if (dbConfig.client.includes('mysql')) { + // MySQL doesn't support the syntax we need to do this in a single query, + // http://dev.mysql.com/doc/refman/5.6/en/update.html + const results = await tx('refresh_state') + .select('entity_id') + .whereIn('entity_ref', function parents(builder) { + return builder + .from('refresh_state') + .innerJoin( + 'refresh_state_references', + { + 'refresh_state_references.target_entity_ref': + 'refresh_state.entity_ref', + }, + ) + .where('refresh_state.entity_id', '=', uid) + .select('refresh_state_references.source_entity_ref'); + }); + await tx('refresh_state') + .update({ + result_hash: 'child-was-deleted', + next_update_at: tx.fn.now(), }) - .where('relations.originating_entity_id', '=', uid) - .andWhere('refresh_state.entity_id', '!=', uid) - .select({ ref: 'relations.source_entity_ref' }), - ); + .whereIn( + 'entity_id', + results.map(key => key.entity_id), + ); + } else { + await tx('refresh_state') + .update({ + result_hash: 'child-was-deleted', + next_update_at: tx.fn.now(), + }) + .whereIn('entity_ref', function parents(builder) { + return builder + .from('refresh_state') + .innerJoin( + 'refresh_state_references', + { + 'refresh_state_references.target_entity_ref': + 'refresh_state.entity_ref', + }, + ) + .where('refresh_state.entity_id', '=', uid) + .select('refresh_state_references.source_entity_ref'); + }); + } - await this.database('refresh_state') - .where('entity_id', uid) - .delete(); + const relationPeers = await tx + .from('relations') + .innerJoin('refresh_state', { + 'refresh_state.entity_ref': 'relations.target_entity_ref', + }) + .where('relations.originating_entity_id', '=', uid) + .andWhere('refresh_state.entity_id', '!=', uid) + .select({ ref: 'relations.target_entity_ref' }) + .union(other => + other + .from('relations') + .innerJoin('refresh_state', { + 'refresh_state.entity_ref': 'relations.source_entity_ref', + }) + .where('relations.originating_entity_id', '=', uid) + .andWhere('refresh_state.entity_id', '!=', uid) + .select({ ref: 'relations.source_entity_ref' }), + ); - await this.stitcher.stitch({ - entityRefs: new Set(relationPeers.map(p => p.ref)), + await tx('refresh_state') + .where('entity_id', uid) + .delete(); + + return new Set(relationPeers.map(p => p.ref)); }); + + if (relationPeerRefs.size > 0) { + await this.stitcher.stitch({ + entityRefs: relationPeerRefs, + }); + } } async entityAncestry(rootRef: string): Promise { @@ -687,9 +689,10 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { }) .groupBy(['search.key', 'search.original_value']); - if (request.filter) { + if (request.filter || request.query) { applyEntityFilterToQuery({ filter: request.filter, + query: request.query, targetQuery: query, onEntityIdField: 'search.entity_id', knex: this.database, @@ -713,40 +716,24 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } } -const entityFilterParser: z.ZodSchema = z.lazy(() => - z - .object({ - key: z.string(), - values: z.array(z.string()).optional(), - }) - .or(z.object({ not: entityFilterParser })) - .or(z.object({ anyOf: z.array(entityFilterParser) })) - .or(z.object({ allOf: z.array(entityFilterParser) })), -); - -export const cursorParser: z.ZodSchema = z.object({ - orderFields: z.array( - z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }), - ), - orderFieldValues: z.array(z.string().or(z.null())), - filter: entityFilterParser.optional(), - isPrevious: z.boolean(), - query: z.string().optional(), - firstSortFieldValues: z.array(z.string().or(z.null())).optional(), - totalItems: z.number().optional(), -}); - function parseCursorFromRequest( request?: QueryEntitiesRequest, ): Partial & { skipTotalItems: boolean } { if (isQueryEntitiesInitialRequest(request)) { const { filter, + query, orderFields: sortFields = [], fullTextFilter, skipTotalItems = false, } = request; - return { filter, orderFields: sortFields, fullTextFilter, skipTotalItems }; + return { + filter, + query, + orderFields: sortFields, + fullTextFilter, + skipTotalItems, + }; } if (isQueryEntitiesCursorRequest(request)) { return { diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index 822706ada4..3cce0efc3f 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -257,10 +257,13 @@ describe('DefaultLocationServiceTest', () => { type: 'url', }, }); - expect(store.createLocation).toHaveBeenCalledWith({ - target: 'https://backstage.io/catalog-info.yaml', - type: 'url', - }); + expect(store.createLocation).toHaveBeenCalledWith( + { + target: 'https://backstage.io/catalog-info.yaml', + type: 'url', + }, + expect.anything(), + ); }); it('should create location with unknown type if configuration allows it', async () => { @@ -279,6 +282,7 @@ describe('DefaultLocationServiceTest', () => { orchestrator, { allowedLocationTypes: ['url', 'unknown'], + defaultLocationConflictStrategy: 'reject', }, ); await expect( @@ -291,10 +295,13 @@ describe('DefaultLocationServiceTest', () => { type: 'unknown', }, }); - expect(store.createLocation).toHaveBeenCalledWith({ - target: 'https://backstage.io/catalog-info.yaml', - type: 'unknown', - }); + expect(store.createLocation).toHaveBeenCalledWith( + { + target: 'https://backstage.io/catalog-info.yaml', + type: 'unknown', + }, + expect.anything(), + ); }); it('should not allow locations of unknown types by default', async () => { @@ -309,6 +316,31 @@ describe('DefaultLocationServiceTest', () => { ).rejects.toThrow(InputError); }); + it('should pass onConflict through to store', async () => { + const locationSpec = { + type: 'url', + target: 'https://backstage.io/catalog-info.yaml', + }; + + store.createLocation.mockResolvedValueOnce({ + id: 'existing-id', + ...locationSpec, + }); + + const result = await locationService.createLocation(locationSpec, false, { + onConflict: 'refresh', + credentials: {} as any, + }); + + expect(result).toEqual({ + location: { id: 'existing-id', ...locationSpec }, + entities: [], + }); + expect(store.createLocation).toHaveBeenCalledWith(locationSpec, { + onConflict: 'refresh', + }); + }); + it('should return default InputError for failed processed entities in dryRun mode', async () => { store.listLocations.mockResolvedValueOnce([]); diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index ad3044ffd9..7effaa2e88 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -33,6 +33,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; export type DefaultLocationServiceOptions = { allowedLocationTypes: string[]; + defaultLocationConflictStrategy: 'refresh' | 'reject'; }; export class DefaultLocationService implements LocationService { @@ -45,6 +46,7 @@ export class DefaultLocationService implements LocationService { orchestrator: CatalogProcessingOrchestrator, options: DefaultLocationServiceOptions = { allowedLocationTypes: ['url'], + defaultLocationConflictStrategy: 'reject', }, ) { this.store = store; @@ -55,6 +57,10 @@ export class DefaultLocationService implements LocationService { async createLocation( input: LocationInput, dryRun: boolean, + options?: { + onConflict?: 'refresh' | 'reject'; + credentials?: BackstageCredentials; + }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { if (!this.options.allowedLocationTypes.includes(input.type)) { throw new InputError( @@ -66,7 +72,10 @@ export class DefaultLocationService implements LocationService { if (dryRun) { return this.dryRunCreateLocation(input); } - const location = await this.store.createLocation(input); + const location = await this.store.createLocation(input, { + onConflict: + options?.onConflict ?? this.options.defaultLocationConflictStrategy, + }); return { location, entities: [] }; } diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index fffe2467b5..00ce749b4a 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -38,6 +38,7 @@ import { DefaultRefreshService } from './DefaultRefreshService'; import { ConfigReader } from '@backstage/config'; import { DefaultStitcher } from '../stitching/DefaultStitcher'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -58,6 +59,7 @@ describe('DefaultRefreshService', () => { logger, refreshInterval: () => 100, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }), catalogDb: new DefaultCatalogDatabase({ database: knex, @@ -115,6 +117,7 @@ describe('DefaultRefreshService', () => { const stitcher = DefaultStitcher.fromConfig(new ConfigReader({}), { knex, logger: defaultLogger, + metrics: metricsServiceMock.mock(), }); const engine = new DefaultCatalogProcessingEngine({ config: new ConfigReader({}), @@ -164,6 +167,7 @@ describe('DefaultRefreshService', () => { createHash: () => createHash('sha1'), pollingIntervalMs: 50, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); return engine; diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index aa3ed3a551..88cd5abde3 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -479,6 +479,87 @@ describe('createRouter readonly disabled', () => { }); }); + describe('POST /entities/by-query', () => { + it('queries entities with a predicate filter', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + entitiesCatalog.queryEntities.mockResolvedValue({ + items: { type: 'object', entities: items }, + pageInfo: {}, + totalItems: 1, + }); + + const response = await request(app) + .post('/entities/by-query') + .send({ query: { kind: 'b' }, limit: 10 }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + items, + totalItems: 1, + pageInfo: {}, + }); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith( + expect.objectContaining({ + query: { kind: 'b' }, + limit: 10, + credentials: mockCredentials.user(), + }), + ); + }); + + it('queries entities with an offset', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + entitiesCatalog.queryEntities.mockResolvedValue({ + items: { type: 'object', entities: items }, + pageInfo: {}, + totalItems: 5, + }); + + const response = await request(app) + .post('/entities/by-query') + .send({ query: { kind: 'b' }, limit: 2, offset: 3 }); + + expect(response.status).toEqual(200); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith( + expect.objectContaining({ + query: { kind: 'b' }, + limit: 2, + offset: 3, + credentials: mockCredentials.user(), + }), + ); + }); + + it('paginates with a cursor in the body', async () => { + const items: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + const cursor = mockCursor({ totalItems: 100, isPrevious: false }); + + entitiesCatalog.queryEntities.mockResolvedValue({ + items: { type: 'object', entities: items }, + pageInfo: { nextCursor: mockCursor() }, + totalItems: 100, + }); + + const response = await request(app) + .post('/entities/by-query') + .send({ cursor: encodeCursor(cursor) }); + + expect(response.status).toEqual(200); + expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith( + expect.objectContaining({ + cursor, + credentials: mockCredentials.user(), + }), + ); + }); + }); + describe('GET /entities/by-uid/:uid', () => { it('can fetch entity by uid', async () => { const entity: Entity = { @@ -644,6 +725,85 @@ describe('createRouter readonly disabled', () => { }); }); + describe('POST /entities/by-refs with query predicate', () => { + it('can fetch entities by refs with a predicate query', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'component', + metadata: { + name: 'a', + }, + }; + const entityRef = stringifyEntityRef(entity); + entitiesCatalog.entitiesBatch.mockResolvedValue({ + items: { type: 'object', entities: [entity] }, + }); + const response = await request(app) + .post('/entities/by-refs') + .set('Content-Type', 'application/json') + .send( + JSON.stringify({ + entityRefs: [entityRef], + query: { kind: 'Component' }, + }), + ); + expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledWith({ + entityRefs: [entityRef], + fields: undefined, + credentials: mockCredentials.user(), + filter: undefined, + query: { kind: 'Component' }, + }); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ items: [entity] }); + }); + + it('forwards both filter query param and body query predicate independently', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'component', + metadata: { + name: 'a', + }, + }; + const entityRef = stringifyEntityRef(entity); + entitiesCatalog.entitiesBatch.mockResolvedValue({ + items: { type: 'object', entities: [entity] }, + }); + const response = await request(app) + .post('/entities/by-refs?filter=kind=Component') + .set('Content-Type', 'application/json') + .send( + JSON.stringify({ + entityRefs: [entityRef], + query: { 'metadata.namespace': 'default' }, + }), + ); + expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledWith({ + entityRefs: [entityRef], + fields: undefined, + credentials: mockCredentials.user(), + filter: { key: 'kind', values: ['Component'] }, + query: { 'metadata.namespace': 'default' }, + }); + expect(response.status).toEqual(200); + }); + + it('rejects invalid query predicate', async () => { + const response = await request(app) + .post('/entities/by-refs') + .set('Content-Type', 'application/json') + .send( + JSON.stringify({ + entityRefs: ['component:default/a'], + query: { $invalid: 'bad' }, + }), + ); + expect(response.status).toEqual(400); + }); + }); + describe('GET /locations', () => { it('happy path: lists locations', async () => { const locations: Location[] = [ @@ -1109,6 +1269,89 @@ describe('createRouter readonly disabled', () => { ); }); }); + + describe('GET /entity-facets', () => { + it('returns facets', async () => { + entitiesCatalog.facets.mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }); + + const response = await request(app).get('/entity-facets?facet=kind'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }); + }); + + it('returns facets with filter parameter', async () => { + entitiesCatalog.facets.mockResolvedValue({ + facets: { 'spec.type': [{ value: 'service', count: 3 }] }, + }); + + const response = await request(app).get( + '/entity-facets?facet=spec.type&filter=kind=Component', + ); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + facets: { 'spec.type': [{ value: 'service', count: 3 }] }, + }); + expect(entitiesCatalog.facets).toHaveBeenCalledWith( + expect.objectContaining({ + facets: ['spec.type'], + filter: { key: 'kind', values: ['Component'] }, + }), + ); + }); + }); + + describe('POST /entity-facets', () => { + it('returns facets with predicate query', async () => { + entitiesCatalog.facets.mockResolvedValue({ + facets: { 'spec.type': [{ value: 'service', count: 3 }] }, + }); + + const response = await request(app) + .post('/entity-facets') + .send({ + facets: ['spec.type'], + query: { kind: 'Component' }, + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + facets: { 'spec.type': [{ value: 'service', count: 3 }] }, + }); + expect(entitiesCatalog.facets).toHaveBeenCalledWith( + expect.objectContaining({ + facets: ['spec.type'], + query: { kind: 'Component' }, + }), + ); + }); + + it('returns facets without query predicate', async () => { + entitiesCatalog.facets.mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }); + + const response = await request(app) + .post('/entity-facets') + .send({ facets: ['kind'] }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }); + }); + + it('returns 400 for missing facets', async () => { + const response = await request(app) + .post('/entity-facets') + .send({ query: { kind: 'Component' } }); + + expect(response.status).toBe(400); + }); + }); }); describe('createRouter readonly and raw json enabled', () => { @@ -1429,6 +1672,7 @@ describe('POST /locations/by-query works end to end', () => { const mockScmEvents = { subscribe: jest.fn(), publish: jest.fn(), + markEventActionTaken: jest.fn(), }; const store = new DefaultLocationStore(knex, mockScmEvents, { @@ -1441,7 +1685,10 @@ describe('POST /locations/by-query works end to end', () => { const locationService = new DefaultLocationService( store, { process: jest.fn() }, - { allowedLocationTypes: ['url'] }, + { + allowedLocationTypes: ['url'], + defaultLocationConflictStrategy: 'reject', + }, ); const router = await createRouter({ diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 11e459ae82..89377c9e12 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -47,6 +47,7 @@ import { parseQueryEntitiesParams, } from './request'; import { parseEntityFacetParams } from './request/parseEntityFacetParams'; +import { parseEntityFacetsQuery } from './request/parseEntityFacetsQuery'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; import { parseEntityPaginationParams } from './request/parseEntityPaginationParams'; import { @@ -65,6 +66,7 @@ import { encodeLocationQueryCursor, parseLocationQuery, } from './request/parseLocationQuery'; +import { parseEntityQuery } from './request/parseEntityQuery'; /** * Options used by {@link createRouter}. @@ -258,6 +260,59 @@ export async function createRouter( throw err; } }) + .post('/entities/by-query', async (req, res) => { + const auditorEvent = await auditor.createEvent({ + eventId: 'entity-fetch', + request: req, + meta: { + queryType: 'by-query', + }, + }); + + try { + const credentials = await httpAuth.credentials(req); + const { fields: rawFields, ...parsed } = parseEntityQuery( + req.body ?? {}, + ); + const fields = rawFields?.length + ? parseEntityTransformParams({ fields: rawFields }) + : undefined; + + const { items, pageInfo, totalItems } = + await entitiesCatalog.queryEntities({ + credentials, + fields, + ...parsed, + }); + + const meta = { + totalItems, + pageInfo: { + ...(pageInfo.nextCursor && { + nextCursor: encodeCursor(pageInfo.nextCursor), + }), + ...(pageInfo.prevCursor && { + prevCursor: encodeCursor(pageInfo.prevCursor), + }), + }, + }; + + await auditorEvent?.success({ meta }); + + await writeEntitiesResponse({ + res, + items, + alwaysUseObjectMode: enableRelationsCompatibility, + responseWrapper: entities => ({ + items: entities, + ...meta, + }), + }); + } catch (err) { + await auditorEvent?.fail({ error: err }); + throw err; + } + }) .get('/entities/by-query', async (req, res) => { const auditorEvent = await auditor.createEvent({ eventId: 'entity-fetch', @@ -470,6 +525,7 @@ export async function createRouter( const { items } = await entitiesCatalog.entitiesBatch({ entityRefs: request.entityRefs, filter: parseEntityFilterParams(req.query), + query: request.query, fields: parseEntityTransformParams(req.query, request.fields), credentials: await httpAuth.credentials(req), }); @@ -510,6 +566,31 @@ export async function createRouter( await auditorEvent?.success(); + res.status(200).json(response); + } catch (err) { + await auditorEvent?.fail({ + error: err, + }); + throw err; + } + }) + .post('/entity-facets', async (req, res) => { + const auditorEvent = await auditor.createEvent({ + eventId: 'entity-facets', + request: req, + }); + + try { + const { facets, query } = parseEntityFacetsQuery(req.body ?? {}); + + const response = await entitiesCatalog.facets({ + query, + facets, + credentials: await httpAuth.credentials(req), + }); + + await auditorEvent?.success(); + res.status(200).json(response); } catch (err) { await auditorEvent?.fail({ @@ -525,6 +606,7 @@ export async function createRouter( .post('/locations', async (req, res) => { const location = await validateRequestBody(req, locationInput); const dryRun = yn(req.query.dryRun, { default: false }); + const onConflict = req.query.onConflict; const auditorEvent = await auditor.createEvent({ eventId: 'location-mutate', @@ -548,6 +630,7 @@ export async function createRouter( location, dryRun, { + onConflict, credentials: await httpAuth.credentials(req), }, ); diff --git a/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.test.ts b/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.test.ts index ea64f5b400..9b540c95b7 100644 --- a/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.test.ts +++ b/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.test.ts @@ -98,7 +98,6 @@ describe.each(databases.eachSupportedId())( entity_ref: entityRef, final_entity: entityJson, hash: 'h', - stitch_ticket: '', }); const search = await buildEntitySearch(id, entity); diff --git a/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.ts b/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.ts index b2a7fb6708..49356cd0ff 100644 --- a/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.ts +++ b/plugins/catalog-backend/src/service/request/applyEntityFilterToQuery.ts @@ -18,8 +18,10 @@ import { EntitiesSearchFilter, EntityFilter, } from '@backstage/plugin-catalog-node'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { Knex } from 'knex'; import { DbSearchRow } from '../../database/tables'; +import { applyPredicateEntityFilterToQuery } from './applyPredicateEntityFilterToQuery'; function isEntitiesSearchFilter( filter: EntitiesSearchFilter | EntityFilter, @@ -118,13 +120,29 @@ function applyInStrategy( // The actual exported function export function applyEntityFilterToQuery(options: { - filter: EntityFilter; + filter?: EntityFilter; + query?: FilterPredicate; targetQuery: Knex.QueryBuilder; onEntityIdField: string; knex: Knex; strategy?: 'in' | 'join'; }): Knex.QueryBuilder { - const { filter, targetQuery, onEntityIdField, knex } = options; + const { filter, query, targetQuery, onEntityIdField, knex } = options; - return applyInStrategy(filter, targetQuery, onEntityIdField, knex, false); + let result = targetQuery; + + if (filter) { + result = applyInStrategy(filter, result, onEntityIdField, knex, false); + } + + if (query) { + result = applyPredicateEntityFilterToQuery({ + filter: query, + targetQuery: result, + onEntityIdField, + knex, + }); + } + + return result; } diff --git a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts new file mode 100644 index 0000000000..6755ed5cc2 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts @@ -0,0 +1,429 @@ +/* + * Copyright 2026 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 { TestDatabases } from '@backstage/backend-test-utils'; +import { applyEntityFilterToQuery } from './applyEntityFilterToQuery'; +import { + DbFinalEntitiesRow, + DbRefreshStateRow, + DbSearchRow, +} from '../../database/tables'; +import { Knex } from 'knex'; +import { applyDatabaseMigrations } from '../../database/migrations'; +import { FilterPredicate } from '@backstage/filter-predicates'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { v4 as uuid } from 'uuid'; +import { buildEntitySearch } from '../../database/operations/stitcher/buildEntitySearch'; + +jest.setTimeout(60_000); + +const databases = TestDatabases.create(); + +describe.each(databases.eachSupportedId())( + 'applyEntityFilterToQuery with predicate queries, %p', + databaseId => { + let knex: Knex; + + beforeAll(async () => { + knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + await addEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'service-a', namespace: 'default' }, + spec: { type: 'service', lifecycle: 'production', owner: 'team-a' }, + relations: [ + { type: 'ownedBy', targetRef: 'group:default/team-a' }, + { type: 'consumesApi', targetRef: 'api:default/api-d' }, + ], + }); + await addEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'service-b', namespace: 'default' }, + spec: { type: 'service', lifecycle: 'experimental', owner: 'team-b' }, + relations: [{ type: 'ownedBy', targetRef: 'group:default/team-b' }], + }); + await addEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'website-c', namespace: 'default' }, + spec: { type: 'website', lifecycle: 'production', owner: 'team-a' }, + relations: [{ type: 'ownedBy', targetRef: 'group:default/team-a' }], + }); + await addEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { name: 'api-d', namespace: 'default' }, + spec: { type: 'openapi', lifecycle: 'production' }, + }); + await addEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'bare-e', + namespace: 'default', + tags: ['java', 'backend'], + }, + spec: {}, + }); + }); + + afterAll(async () => { + knex.destroy(); + }); + + async function addEntity(entity: Entity) { + const id = uuid(); + const entityRef = stringifyEntityRef(entity); + const entityJson = JSON.stringify(entity); + + await knex('refresh_state').insert({ + entity_id: id, + entity_ref: entityRef, + unprocessed_entity: entityJson, + errors: '[]', + next_update_at: '2031-01-01 23:00:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + + await knex('final_entities').insert({ + entity_id: id, + entity_ref: entityRef, + final_entity: entityJson, + hash: 'h', + }); + + const search = await buildEntitySearch(id, entity); + await knex('search').insert(search); + } + + async function query(predicate: FilterPredicate): Promise { + const q = + knex('final_entities').whereNotNull('final_entity'); + applyEntityFilterToQuery({ + query: predicate, + targetQuery: q, + onEntityIdField: 'final_entities.entity_id', + knex, + }); + return await q.then(rows => + rows.map(row => JSON.parse(row.final_entity!).metadata.name).toSorted(), + ); + } + + describe('field expressions', () => { + it('matches everything for empty field expression', async () => { + await expect(query({})).resolves.toEqual([ + 'api-d', + 'bare-e', + 'service-a', + 'service-b', + 'website-c', + ]); + }); + + it('filters by direct field value', async () => { + await expect(query({ kind: 'component' })).resolves.toEqual([ + 'bare-e', + 'service-a', + 'service-b', + 'website-c', + ]); + }); + + it('filters by exact spec field value', async () => { + await expect(query({ 'spec.type': 'service' })).resolves.toEqual([ + 'service-a', + 'service-b', + ]); + }); + + it('throws on unsupported field operator', async () => { + await expect(query({ kind: { $bad: 'value' } as any })).rejects.toThrow( + /\$contains operator, but got/, + ); + }); + + it('throws on top-level primitive', async () => { + await expect(query('bad-value' as any)).rejects.toThrow( + /top-level primitive values are not supported/, + ); + }); + }); + + describe('$all', () => { + it('filters with $all', async () => { + await expect( + query({ + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }), + ).resolves.toEqual(['service-a', 'service-b']); + }); + + it('matches everything for empty $all', async () => { + await expect(query({ $all: [] })).resolves.toEqual([ + 'api-d', + 'bare-e', + 'service-a', + 'service-b', + 'website-c', + ]); + }); + }); + + describe('$any', () => { + it('filters with $any', async () => { + await expect( + query({ + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }), + ).resolves.toEqual(['service-a', 'service-b', 'website-c']); + }); + + it('returns nothing for empty $any', async () => { + await expect(query({ $any: [] })).resolves.toEqual([]); + }); + }); + + describe('$not', () => { + it('filters with $not', async () => { + await expect( + query({ + $all: [ + { kind: 'component' }, + { $not: { 'spec.lifecycle': 'experimental' } }, + ], + }), + ).resolves.toEqual(['bare-e', 'service-a', 'website-c']); + }); + + it('matches nothing for {$not: {}}', async () => { + await expect(query({ $not: {} })).resolves.toEqual([]); + }); + }); + + describe('$in', () => { + it('filters with $in', async () => { + await expect( + query({ 'spec.type': { $in: ['service', 'openapi'] } }), + ).resolves.toEqual(['api-d', 'service-a', 'service-b']); + }); + }); + + describe('$exists', () => { + it('filters with $exists true', async () => { + await expect( + query({ + $all: [{ kind: 'component' }, { 'spec.owner': { $exists: true } }], + }), + ).resolves.toEqual(['service-a', 'service-b', 'website-c']); + }); + + it('filters with $exists false', async () => { + await expect( + query({ + $all: [{ kind: 'component' }, { 'spec.owner': { $exists: false } }], + }), + ).resolves.toEqual(['bare-e']); + }); + }); + + describe('$hasPrefix', () => { + it('filters with $hasPrefix', async () => { + await expect( + query({ 'metadata.name': { $hasPrefix: 'service' } }), + ).resolves.toEqual(['service-a', 'service-b']); + }); + + it('filters with $hasPrefix case-insensitively', async () => { + await expect( + query({ 'metadata.name': { $hasPrefix: 'Service' } }), + ).resolves.toEqual(['service-a', 'service-b']); + }); + }); + + describe('$contains', () => { + it('filters with primitive $contains on array fields', async () => { + await expect( + query({ 'metadata.tags': { $contains: 'java' } }), + ).resolves.toEqual(['bare-e']); + }); + + it('matches relations by type only (existence)', async () => { + await expect( + query({ relations: { $contains: { type: 'consumesApi' } } }), + ).resolves.toEqual(['service-a']); + }); + + it('matches relations by type and exact targetRef', async () => { + await expect( + query({ + relations: { + $contains: { + type: 'ownedBy', + targetRef: 'group:default/team-a', + }, + }, + }), + ).resolves.toEqual(['service-a', 'website-c']); + }); + + it('matches relations by type and targetRef case-insensitively', async () => { + await expect( + query({ + relations: { + $contains: { + type: 'OwnedBy', + targetRef: 'Group:Default/Team-A', + }, + }, + }), + ).resolves.toEqual(['service-a', 'website-c']); + }); + + it('handles mixed-case keys in $contains object', async () => { + await expect( + query({ + relations: { + $contains: { + TyPe: 'ownedBy', + TargetRef: 'group:default/team-a', + }, + }, + } as any), + ).resolves.toEqual(['service-a', 'website-c']); + }); + + it('matches relations by type and targetRef with $in', async () => { + await expect( + query({ + relations: { + $contains: { + type: 'ownedBy', + targetRef: { + $in: ['group:default/team-a', 'group:default/team-b'], + }, + }, + }, + }), + ).resolves.toEqual(['service-a', 'service-b', 'website-c']); + }); + + it('throws on unsupported keys in $contains object', async () => { + await expect( + query({ + relations: { + $contains: { type: 'ownedBy', badKey: 'value' }, + } as any, + }), + ).rejects.toThrow(/Unsupported key "badKey" in \$contains/); + }); + + it('throws on duplicate keys in $contains object', async () => { + await expect( + query({ + relations: { + // JSON.parse allows duplicate keys, last one wins, but we + // simulate via an object that has both casings + $contains: JSON.parse('{"type": "ownedBy", "Type": "ownedBy"}'), + }, + }), + ).rejects.toThrow(/Duplicate key "Type" in \$contains/); + }); + + it('throws on $contains object without type', async () => { + await expect( + query({ + relations: { + $contains: { targetRef: 'group:default/team-a' }, + } as any, + }), + ).rejects.toThrow(/requires a "type" string property/); + }); + + it('throws on empty $in array in targetRef', async () => { + await expect( + query({ + relations: { + $contains: { type: 'ownedBy', targetRef: { $in: [] } }, + }, + }), + ).rejects.toThrow(/Empty "\$in" array for \$contains on "relations"/); + }); + + it('throws on unsupported targetRef value', async () => { + await expect( + query({ + relations: { + $contains: { type: 'ownedBy', targetRef: ['bad'] }, + } as any, + }), + ).rejects.toThrow(/Unsupported value in \$contains for "relations"/); + }); + + it('throws on object $contains for non-relations fields', async () => { + await expect( + query({ + 'metadata.tags': { + $contains: { type: 'something' }, + } as any, + }), + ).rejects.toThrow( + /Object form of \$contains is not supported for field/, + ); + }); + }); + + describe('nested operators', () => { + it('handles nested logical operators', async () => { + await expect( + query({ + $all: [ + { kind: 'component' }, + { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + { $not: { 'spec.lifecycle': 'experimental' } }, + ], + }), + ).resolves.toEqual(['service-a', 'website-c']); + }); + }); + + describe('combined filter and query', () => { + it('combines filter and query independently', async () => { + const q = + knex('final_entities').whereNotNull( + 'final_entity', + ); + applyEntityFilterToQuery({ + filter: { key: 'kind', values: ['component'] }, + query: { 'spec.type': 'service' }, + targetQuery: q, + onEntityIdField: 'final_entities.entity_id', + knex, + }); + const result = await q.then(rows => + rows + .map(row => JSON.parse(row.final_entity!).metadata.name) + .toSorted(), + ); + expect(result).toEqual(['service-a', 'service-b']); + }); + }); + }, +); diff --git a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts new file mode 100644 index 0000000000..d915b8c4b5 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts @@ -0,0 +1,329 @@ +/* + * Copyright 2026 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 { + FilterPredicate, + FilterPredicatePrimitive, + FilterPredicateValue, +} from '@backstage/filter-predicates'; +import { InputError } from '@backstage/errors'; +import { Knex } from 'knex'; +import { DbSearchRow } from '../../database/tables'; + +function isPrimitive(value: unknown): value is FilterPredicatePrimitive { + return ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ); +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function applyPredicateEntityFilterToQuery(options: { + filter: FilterPredicate; + targetQuery: Knex.QueryBuilder; + onEntityIdField: string; + knex: Knex; +}): Knex.QueryBuilder { + const { filter, targetQuery, onEntityIdField, knex } = options; + + // We do not support top-level primitives; all matching happens through objects + if (!isObject(filter)) { + const actual = JSON.stringify(filter); + throw new InputError( + `Invalid filter predicate: top-level primitive values are not supported. Wrap the value in a field expression, e.g. { "kind": ${actual} }`, + ); + } + + if ('$not' in filter) { + return targetQuery.andWhereNot(inner => + applyPredicateEntityFilterToQuery({ + filter: filter.$not, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); + } + + if ('$all' in filter) { + if (filter.$all.length === 0) { + return targetQuery.andWhereRaw('1 = 1'); + } + return targetQuery.andWhere(outer => { + for (const subFilter of filter.$all) { + outer.andWhere(inner => + applyPredicateEntityFilterToQuery({ + filter: subFilter, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); + } + }); + } + + if ('$any' in filter) { + if (filter.$any.length === 0) { + return targetQuery.andWhereRaw('1 = 0'); + } + return targetQuery.andWhere(outer => { + for (const subFilter of filter.$any) { + outer.orWhere(inner => + applyPredicateEntityFilterToQuery({ + filter: subFilter, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); + } + }); + } + + // Treat the filter as a field expression like { "kind": "component" } or { "spec.type": { "$in": ["service", "website"] } } + if (Object.keys(filter).length === 0) { + return targetQuery.andWhereRaw('1 = 1'); + } + return targetQuery.andWhere(inner => { + for (const [keyAnyCase, value] of Object.entries(filter)) { + applyFieldCondition({ + key: keyAnyCase.toLocaleLowerCase('en-US'), + value, + targetQuery: inner, + onEntityIdField, + knex, + }); + } + }); +} + +/** + * Applies a single { key: value } filter to the target query. + */ +function applyFieldCondition(options: { + key: string; + value: FilterPredicateValue; + targetQuery: Knex.QueryBuilder; + onEntityIdField: string; + knex: Knex; +}): Knex.QueryBuilder { + const { key, value, targetQuery, onEntityIdField, knex } = options; + + if (isPrimitive(value)) { + const matchQuery = knex('search') + .select('search.entity_id') + .where({ + key, + value: String(value).toLocaleLowerCase('en-US'), + }); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + if (isObject(value)) { + if ('$exists' in value) { + const existsQuery = knex('search') + .select('search.entity_id') + .where({ key }); + return targetQuery.andWhere( + onEntityIdField, + value.$exists ? 'in' : 'not in', + existsQuery, + ); + } + + if ('$in' in value) { + const values = value.$in.map(v => String(v).toLocaleLowerCase('en-US')); + const matchQuery = knex('search') + .select('search.entity_id') + .where({ key }) + .whereIn('value', values); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + if ('$hasPrefix' in value) { + const prefix = value.$hasPrefix.toLocaleLowerCase('en-US'); + const escaped = prefix.replace(/[%_\\]/g, c => `\\${c}`); + const matchQuery = knex('search') + .select('search.entity_id') + .where({ key }) + .andWhereRaw('?? like ? escape ?', ['value', `${escaped}%`, '\\']); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + if ('$contains' in value) { + const target = value.$contains; + + // If the target is a primitive, match on the special array syntax. + // + // FROM: `{ "a": { "$contains": "b" } }` + // + // TO: `{ "a": "b" }` + // + // The search table does not actually show us that "a" was an array to + // begin with, so this can mistakenly also match on an object that had a + // "b" key with a primitive value. We'll consider that an acceptable + // tradeoff though. + if (isPrimitive(target)) { + const matchQuery = knex('search') + .select('search.entity_id') + .where({ + key, + value: String(target).toLocaleLowerCase('en-US'), + }); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + // Object form of $contains - currently only supports relation-style + // objects with "type" and optional "targetRef" keys. + // + // FROM: `{ "relations": { "$contains": { "type": "ownedBy", "targetRef": "group:default/team-a" } } }` + // + // TO: search for key = "relations.ownedby" AND value = "group:default/team-a" + if (isObject(target)) { + if (key === 'relations') { + return applyContainsRelation({ + target, + targetQuery, + onEntityIdField, + knex, + }); + } + + throw new InputError( + `Object form of $contains is not supported for field "${key}"`, + ); + } + + const actual = JSON.stringify(target); + throw new InputError( + `Unsupported $contains target for field "${key}": ${actual}`, + ); + } + } + + const actual = JSON.stringify(value); + throw new InputError( + `Invalid filter predicate value for field "${key}": expected a primitive value, $exists, $in, $hasPrefix, or $contains operator, but got ${actual}`, + ); +} + +/** + * Handles expressions on the form + * + * ``` + * { + * "relations": { + * "$contains": { + * "type": "ownedBy", + * "targetRef": "group:default/team-a" + * } + * } + * } + * ``` + * + * which map onto the search table's special `relation.: ` + * syntax. + * + * Only the keys "type" and "targetRef" are supported. The "type" key is + * required. If "targetRef" is omitted, it becomes an existence check for any + * relation of that type. The "targetRef" value can be a string or an `$in` + * array. + */ +function applyContainsRelation(options: { + target: Record; + targetQuery: Knex.QueryBuilder; + onEntityIdField: string; + knex: Knex; +}): Knex.QueryBuilder { + const { target: rawTarget, targetQuery, onEntityIdField, knex } = options; + + function parseStringOrIn(value: unknown): string[] { + if (typeof value === 'string') { + return [value.toLocaleLowerCase('en-US')]; + } + if ( + isObject(value) && + Object.keys(value).length === 1 && + '$in' in value && + Array.isArray(value.$in) && + value.$in.every((v): v is string => typeof v === 'string') + ) { + if (value.$in.length === 0) { + throw new InputError( + `Empty "$in" array for $contains on "relations" is not allowed`, + ); + } + return value.$in.map(v => v.toLocaleLowerCase('en-US')); + } + const actual = JSON.stringify(value); + throw new InputError( + `Unsupported value in $contains for "relations": expected a string or { "$in": [strings] }, but got ${actual}`, + ); + } + + let type: string | undefined; + let targetRef: string[] | undefined; + + for (const [rawKey, value] of Object.entries(rawTarget)) { + const key = rawKey.toLocaleLowerCase('en-US'); + + if (key === 'type') { + if (type !== undefined) { + throw new InputError( + `Duplicate key "${rawKey}" in $contains for "relations"`, + ); + } + if (typeof value !== 'string') { + throw new InputError( + `The $contains operator for "relations" requires a "type" string property`, + ); + } + type = value; + } else if (key === 'targetref') { + if (targetRef !== undefined) { + throw new InputError( + `Duplicate key "${rawKey}" in $contains for "relations"`, + ); + } + targetRef = parseStringOrIn(value); + } else { + throw new InputError( + `Unsupported key "${rawKey}" in $contains for "relations". Only "type" and "targetRef" are supported`, + ); + } + } + + if (!type) { + throw new InputError( + `The $contains operator for "relations" requires a "type" string property`, + ); + } + + const matchQuery = knex('search') + .select('search.entity_id') + .where({ key: `relations.${type.toLocaleLowerCase('en-US')}` }); + + if (targetRef) { + matchQuery.whereIn('value', targetRef); + } + + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); +} diff --git a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts index d5469315be..02ad1d5566 100644 --- a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts +++ b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts @@ -15,20 +15,36 @@ */ import { InputError } from '@backstage/errors'; +import { + createZodV3FilterPredicateSchema, + FilterPredicate, +} from '@backstage/filter-predicates'; import { Request } from 'express'; -import { z } from 'zod'; +import { z } from 'zod/v3'; +import { fromZodError } from 'zod-validation-error/v3'; + +const filterPredicateSchema = createZodV3FilterPredicateSchema(z); const schema = z.object({ entityRefs: z.array(z.string()), fields: z.array(z.string()).optional(), + query: filterPredicateSchema.optional(), }); -export function entitiesBatchRequest(req: Request): z.infer { - try { - return schema.parse(req.body); - } catch (error) { +export interface ParsedEntitiesBatchRequest { + entityRefs: string[]; + fields?: string[]; + query?: FilterPredicate; +} + +export function entitiesBatchRequest(req: Request): ParsedEntitiesBatchRequest { + const result = schema.safeParse(req.body); + if (!result.success) { throw new InputError( - `Malformed request body (did you remember to specify an application/json content type?), ${error.message}`, + `Malformed request body (did you remember to specify an application/json content type?), ${fromZodError( + result.error, + )}`, ); } + return result.data; } diff --git a/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.test.ts b/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.test.ts new file mode 100644 index 0000000000..473466f950 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2026 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 { parseEntityFacetsQuery } from './parseEntityFacetsQuery'; + +describe('parseEntityFacetsQuery', () => { + it('parses facets with no query', () => { + expect(parseEntityFacetsQuery({ facets: ['kind'] })).toEqual({ + facets: ['kind'], + query: undefined, + }); + }); + + it('parses facets with a simple query', () => { + expect( + parseEntityFacetsQuery({ + facets: ['spec.type'], + query: { kind: 'Component' }, + }), + ).toEqual({ + facets: ['spec.type'], + query: { kind: 'Component' }, + }); + }); + + it('parses facets with complex predicate query', () => { + const query = { + $all: [{ kind: 'Component' }, { 'spec.lifecycle': 'production' }], + }; + expect(parseEntityFacetsQuery({ facets: ['spec.type'], query })).toEqual({ + facets: ['spec.type'], + query, + }); + }); + + it('throws on missing facets', () => { + expect(() => parseEntityFacetsQuery({} as any)).toThrow( + 'Missing or empty facets parameter', + ); + }); + + it('throws on empty facets array', () => { + expect(() => parseEntityFacetsQuery({ facets: [] })).toThrow( + 'Missing or empty facets parameter', + ); + }); + + it('throws on invalid query (null)', () => { + expect(() => + parseEntityFacetsQuery({ facets: ['kind'], query: null as any }), + ).toThrow(); + }); + + it('throws on invalid query (array)', () => { + expect(() => + parseEntityFacetsQuery({ facets: ['kind'], query: [] as any }), + ).toThrow(); + }); + + it('throws on invalid query (invalid operator)', () => { + expect(() => + parseEntityFacetsQuery({ + facets: ['kind'], + query: { $invalid: 'bad' } as any, + }), + ).toThrow(); + }); +}); diff --git a/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.ts b/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.ts new file mode 100644 index 0000000000..04e5e2ab82 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityFacetsQuery.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2026 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 { InputError } from '@backstage/errors'; +import { + createZodV3FilterPredicateSchema, + FilterPredicate, +} from '@backstage/filter-predicates'; +import { z } from 'zod/v3'; +import { fromZodError } from 'zod-validation-error/v3'; +import { QueryEntityFacetsByPredicateRequest } from '../../schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model'; + +const filterPredicateSchema = createZodV3FilterPredicateSchema(z); + +export interface ParsedEntityFacetsQuery { + facets: string[]; + query?: FilterPredicate; +} + +export function parseEntityFacetsQuery( + request: Readonly, +): ParsedEntityFacetsQuery { + // Parse facets + if (!request.facets || request.facets.length === 0) { + throw new InputError('Missing or empty facets parameter'); + } + const facets = request.facets.filter(f => f.length > 0); + if (facets.length === 0) { + throw new InputError('Missing or empty facets parameter'); + } + + // Parse query predicate + let query: FilterPredicate | undefined; + if (request.query !== undefined) { + const result = filterPredicateSchema.safeParse(request.query); + if (!result.success) { + throw new InputError(`Invalid query: ${fromZodError(result.error)}`); + } + query = result.data; + } + + return { facets, query }; +} diff --git a/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts b/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts new file mode 100644 index 0000000000..7380259188 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityQuery.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright 2026 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 { parseEntityQuery } from './parseEntityQuery'; +import { encodeCursor } from '../util'; +import { Cursor } from '../../catalog/types'; + +describe('parseEntityQuery', () => { + describe('initial request', () => { + it('returns empty result for empty request', () => { + const result = parseEntityQuery({}); + expect(result).toEqual({ + query: undefined, + orderFields: undefined, + fullTextFilter: undefined, + fields: undefined, + limit: undefined, + offset: undefined, + }); + }); + + it('parses a simple query predicate', () => { + const query = { kind: 'component' }; + const result = parseEntityQuery({ query }); + expect(result).toEqual( + expect.objectContaining({ query: { kind: 'component' } }), + ); + }); + + it('parses a complex query with $all, $any, $not', () => { + const query = { + $all: [ + { kind: 'component' }, + { $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }] }, + { $not: { 'spec.lifecycle': 'experimental' } }, + ], + }; + const result = parseEntityQuery({ query }); + expect(result).toEqual(expect.objectContaining({ query })); + }); + + it('parses query with $exists operator', () => { + const query = { 'metadata.labels.team': { $exists: true } }; + const result = parseEntityQuery({ query }); + expect(result).toEqual(expect.objectContaining({ query })); + }); + + it('parses query with $in operator', () => { + const query = { kind: { $in: ['component', 'api'] } }; + const result = parseEntityQuery({ query }); + expect(result).toEqual(expect.objectContaining({ query })); + }); + + it('passes through limit', () => { + const result = parseEntityQuery({ limit: 50 }); + expect(result).toEqual(expect.objectContaining({ limit: 50 })); + }); + + it('passes through offset', () => { + const result = parseEntityQuery({ offset: 100 }); + expect(result).toEqual(expect.objectContaining({ offset: 100 })); + }); + + it('passes through limit and offset together', () => { + const result = parseEntityQuery({ limit: 50, offset: 100 }); + expect(result).toEqual( + expect.objectContaining({ limit: 50, offset: 100 }), + ); + }); + + it('passes through fields', () => { + const result = parseEntityQuery({ + fields: ['metadata.name', 'kind'], + }); + expect(result).toEqual( + expect.objectContaining({ fields: ['metadata.name', 'kind'] }), + ); + }); + + it('parses orderBy into orderFields', () => { + const result = parseEntityQuery({ + orderBy: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'metadata.namespace', order: 'desc' }, + ], + }); + expect(result).toEqual( + expect.objectContaining({ + orderFields: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'metadata.namespace', order: 'desc' }, + ], + }), + ); + }); + + it('parses fullTextFilter', () => { + const result = parseEntityQuery({ + fullTextFilter: { term: 'search term', fields: ['metadata.name'] }, + }); + expect(result).toEqual( + expect.objectContaining({ + fullTextFilter: { + term: 'search term', + fields: ['metadata.name'], + }, + }), + ); + }); + + it('defaults fullTextFilter term to empty string when missing', () => { + const result = parseEntityQuery({ + fullTextFilter: {} as any, + }); + expect(result).toEqual( + expect.objectContaining({ + fullTextFilter: { term: '', fields: undefined }, + }), + ); + }); + + it('throws on invalid query predicate', () => { + expect(() => + parseEntityQuery({ query: { $invalid: true } as any }), + ).toThrow(/Invalid query/); + }); + + it('throws when query root is not an object', () => { + expect(() => parseEntityQuery({ query: 'bad' as any })).toThrow( + /Query must be an object/, + ); + }); + + it('throws on invalid orderBy order value', () => { + expect(() => + parseEntityQuery({ + // @ts-expect-error - invalid order value + orderBy: [{ field: 'metadata.name', order: 'sideways' }], + }), + ).toThrow(/Invalid order field order/); + }); + }); + + describe('cursor request', () => { + function makeCursor(partial: Partial): string { + const full: Cursor = { + orderFields: [], + orderFieldValues: [], + isPrevious: false, + ...partial, + }; + return encodeCursor(full); + } + + it('decodes a valid cursor', () => { + const cursor = makeCursor({ + orderFields: [{ field: 'metadata.name', order: 'asc' }], + orderFieldValues: ['test'], + isPrevious: false, + }); + const result = parseEntityQuery({ cursor }); + expect(result).toEqual( + expect.objectContaining({ + cursor: expect.objectContaining({ + orderFields: [{ field: 'metadata.name', order: 'asc' }], + orderFieldValues: ['test'], + isPrevious: false, + }), + }), + ); + }); + + it('passes through limit and fields with cursor', () => { + const cursor = makeCursor({}); + const result = parseEntityQuery({ + cursor, + limit: 25, + fields: ['metadata.name'], + }); + expect(result).toEqual( + expect.objectContaining({ + limit: 25, + fields: ['metadata.name'], + }), + ); + }); + + it('throws on empty cursor string', () => { + expect(() => parseEntityQuery({ cursor: '' })).toThrow( + /Cursor cannot be empty/, + ); + }); + + it('throws on invalid base64 cursor', () => { + expect(() => parseEntityQuery({ cursor: '!!not-valid!!' })).toThrow( + /Malformed cursor/, + ); + }); + + it('throws on invalid JSON in cursor', () => { + const cursor = Buffer.from('not json', 'utf8').toString('base64'); + expect(() => parseEntityQuery({ cursor })).toThrow(/Malformed cursor/); + }); + }); +}); diff --git a/plugins/catalog-backend/src/service/request/parseEntityQuery.ts b/plugins/catalog-backend/src/service/request/parseEntityQuery.ts new file mode 100644 index 0000000000..27992c273a --- /dev/null +++ b/plugins/catalog-backend/src/service/request/parseEntityQuery.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2026 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 { InputError } from '@backstage/errors'; +import { + createZodV3FilterPredicateSchema, + FilterPredicate, +} from '@backstage/filter-predicates'; +import { z } from 'zod/v3'; +import { fromZodError } from 'zod-validation-error/v3'; +import { QueryEntitiesByPredicateRequest } from '../../schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model'; +import { EntityOrder } from '../../catalog/types'; +import { Cursor } from '../../catalog/types'; +import { decodeCursor } from '../util'; + +const filterPredicateSchema = createZodV3FilterPredicateSchema(z); + +function isSupportedFilterPredicateRoot( + value: FilterPredicate | undefined, +): boolean { + if (value === undefined) { + return true; + } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false; + } + return true; +} + +function parseOrderFields( + orderField: Array<{ field: string; order: string }> | undefined, +): EntityOrder[] | undefined { + if (!orderField?.length) { + return undefined; + } + return orderField.map(({ field, order }) => { + if (order !== 'asc' && order !== 'desc') { + throw new InputError('Invalid order field order, must be asc or desc'); + } + return { field, order }; + }); +} + +export type ParsedEntityQuery = + | { + cursor: Cursor; + fields?: string[]; + limit?: number; + } + | { + query?: FilterPredicate; + orderFields?: EntityOrder[]; + fullTextFilter?: { term: string; fields?: string[] }; + fields?: string[]; + limit?: number; + offset?: number; + }; + +export function parseEntityQuery( + request: Readonly, +): ParsedEntityQuery { + if (request.cursor !== undefined) { + if (!request.cursor) { + throw new InputError('Cursor cannot be empty'); + } + + const cursor = decodeCursor(request.cursor); + return { + cursor, + fields: request.fields, + limit: request.limit, + }; + } + + let query: FilterPredicate | undefined; + if (request.query !== undefined) { + const result = filterPredicateSchema.safeParse(request.query); + if (!result.success) { + throw new InputError(`Invalid query: ${fromZodError(result.error)}`); + } + if (!isSupportedFilterPredicateRoot(result.data)) { + throw new InputError('Query must be an object'); + } + query = result.data; + } + + const orderFields = parseOrderFields(request.orderBy); + + return { + query, + orderFields, + fullTextFilter: request.fullTextFilter + ? { + term: request.fullTextFilter.term ?? '', + fields: request.fullTextFilter.fields, + } + : undefined, + fields: request.fields, + limit: request.limit, + offset: request.offset, + }; +} diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index f452484b72..c949da1eda 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -35,6 +35,7 @@ export interface LocationService { location: LocationInput, dryRun: boolean, options: { + onConflict?: 'refresh' | 'reject'; credentials: BackstageCredentials; }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }>; @@ -84,7 +85,12 @@ export interface RefreshService { * Interacts with the database to manage locations. */ export interface LocationStore { - createLocation(location: LocationInput): Promise; + createLocation( + location: LocationInput, + options?: { + onConflict: 'refresh' | 'reject'; + }, + ): Promise; listLocations(): Promise; queryLocations(options: { limit: number; diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 7e0e8b42f9..708eec2de3 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -15,6 +15,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'; @@ -109,6 +110,8 @@ const entityFilterParser: z.ZodSchema = z.lazy(() => .or(z.object({ allOf: z.array(entityFilterParser) })), ); +const filterPredicateSchema = createZodV3FilterPredicateSchema(z); + export const cursorParser: z.ZodSchema = z.object({ orderFields: z.array( z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }), @@ -122,7 +125,7 @@ export const cursorParser: z.ZodSchema = z.object({ orderFieldValues: z.array(z.string().or(z.null())), filter: entityFilterParser.optional(), isPrevious: z.boolean(), - query: z.string().optional(), + query: filterPredicateSchema.optional(), firstSortFieldValues: z.array(z.string().or(z.null())).optional(), totalItems: z.number().optional(), }); diff --git a/plugins/catalog-backend/src/stitching/DefaultStitcher.test.ts b/plugins/catalog-backend/src/stitching/DefaultStitcher.test.ts index 251aaa9080..88acf68dd3 100644 --- a/plugins/catalog-backend/src/stitching/DefaultStitcher.test.ts +++ b/plugins/catalog-backend/src/stitching/DefaultStitcher.test.ts @@ -25,6 +25,7 @@ import { DbSearchRow, } from '../database/tables'; import { DefaultStitcher } from './DefaultStitcher'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -42,6 +43,7 @@ describe('Stitcher', () => { knex: db, logger, strategy: { mode: 'immediate' }, + metrics: metricsServiceMock.mock(), }); let entities: DbFinalEntitiesRow[]; let entity: Entity; diff --git a/plugins/catalog-backend/src/stitching/DefaultStitcher.ts b/plugins/catalog-backend/src/stitching/DefaultStitcher.ts index 14b3eca0d4..e7848961a6 100644 --- a/plugins/catalog-backend/src/stitching/DefaultStitcher.ts +++ b/plugins/catalog-backend/src/stitching/DefaultStitcher.ts @@ -31,6 +31,7 @@ import { stitchingStrategyFromConfig, } from './types'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; type DeferredStitchItem = Awaited< ReturnType @@ -55,11 +56,13 @@ export class DefaultStitcher implements Stitcher { options: { knex: Knex; logger: LoggerService; + metrics: MetricsService; }, ): DefaultStitcher { return new DefaultStitcher({ knex: options.knex, logger: options.logger, + metrics: options.metrics, strategy: stitchingStrategyFromConfig(config), }); } @@ -67,12 +70,17 @@ export class DefaultStitcher implements Stitcher { constructor(options: { knex: Knex; logger: LoggerService; + metrics: MetricsService; strategy: StitchingStrategy; }) { this.knex = options.knex; this.logger = options.logger; this.strategy = options.strategy; - this.tracker = progressTracker(options.knex, options.logger); + this.tracker = progressTracker( + options.knex, + options.logger, + options.metrics, + ); } async stitch(options: { diff --git a/plugins/catalog-backend/src/stitching/progressTracker.ts b/plugins/catalog-backend/src/stitching/progressTracker.ts index 9e00a1a01b..31a405d9cd 100644 --- a/plugins/catalog-backend/src/stitching/progressTracker.ts +++ b/plugins/catalog-backend/src/stitching/progressTracker.ts @@ -15,31 +15,33 @@ */ import { stringifyError } from '@backstage/errors'; -import { metrics } from '@opentelemetry/api'; import { Knex } from 'knex'; import { DateTime } from 'luxon'; -import { DbRefreshStateRow } from '../database/tables'; +import { DbStitchQueueRow } from '../database/tables'; import { createCounterMetric } from '../util/metrics'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; // Helps wrap the timing and logging behaviors -export function progressTracker(knex: Knex, logger: LoggerService) { +export function progressTracker( + knex: Knex, + logger: LoggerService, + metrics: MetricsService, +) { // prom-client metrics are deprecated in favour of OpenTelemetry metrics. const promStitchedEntities = createCounterMetric({ name: 'catalog_stitched_entities_count', help: 'Amount of entities stitched. DEPRECATED, use OpenTelemetry metrics instead', }); - const meter = metrics.getMeter('default'); - - const stitchedEntities = meter.createCounter( + const stitchedEntities = metrics.createCounter( 'catalog.stitched.entities.count', { description: 'Amount of entities stitched', }, ); - const stitchingDuration = meter.createHistogram( + const stitchingDuration = metrics.createHistogram( 'catalog.stitching.duration', { description: 'Time spent executing the full stitching flow', @@ -47,18 +49,18 @@ export function progressTracker(knex: Knex, logger: LoggerService) { }, ); - const stitchingQueueCount = meter.createObservableGauge( + const stitchingQueueCount = metrics.createObservableGauge( 'catalog.stitching.queue.length', { description: 'Number of entities currently in the stitching queue' }, ); stitchingQueueCount.addCallback(async result => { - const total = await knex('refresh_state') - .count({ count: '*' }) - .whereNotNull('next_stitch_at'); + const total = await knex('stitch_queue').count({ + count: '*', + }); result.observe(Number(total[0].count)); }); - const stitchingQueueDelay = meter.createHistogram( + const stitchingQueueDelay = metrics.createHistogram( 'catalog.stitching.queue.delay', { description: diff --git a/plugins/catalog-backend/src/tests/integration.test.ts b/plugins/catalog-backend/src/tests/integration.test.ts index a213d41efb..d03a23f896 100644 --- a/plugins/catalog-backend/src/tests/integration.test.ts +++ b/plugins/catalog-backend/src/tests/integration.test.ts @@ -56,6 +56,7 @@ import { mockServices, TestDatabases } from '@backstage/backend-test-utils'; import { LoggerService } from '@backstage/backend-plugin-api'; import { entitiesResponseToObjects } from '../service/response'; import { deleteOrphanedEntities } from '../database/operations/util/deleteOrphanedEntities'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; const voidLogger = mockServices.logger.mock(); @@ -248,6 +249,7 @@ class TestHarness { logger, events: mockServices.events.mock(), refreshInterval: () => 0.05, + metrics: metricsServiceMock.mock(), }); const integrations = ScmIntegrations.fromConfig(config); @@ -280,6 +282,7 @@ class TestHarness { const stitcher = DefaultStitcher.fromConfig(config, { knex: options.db, logger, + metrics: metricsServiceMock.mock(), }); const catalog = new DefaultEntitiesCatalog({ database: options.db, @@ -306,6 +309,7 @@ class TestHarness { }, tracker: proxyProgressTracker, events: mockServices.events.mock(), + metrics: metricsServiceMock.mock(), }); const refresh = new DefaultRefreshService({ database: catalogDatabase }); diff --git a/plugins/catalog-backend/src/tests/migrations.test.ts b/plugins/catalog-backend/src/tests/migrations.test.ts index 3807943b86..24a23b2888 100644 --- a/plugins/catalog-backend/src/tests/migrations.test.ts +++ b/plugins/catalog-backend/src/tests/migrations.test.ts @@ -70,6 +70,14 @@ describe('migrations', () => { last_discovery_at: new Date(), }) .into('refresh_state'); + await knex + .insert({ + entity_id: 'i1', + hash: 'h', + final_entity: '{}', + entity_ref: 'k:ns/n1', + }) + .into('final_entities'); await knex .insert({ entity_id: 'i1', key: 'k1', value: 'v1' }) .into('search'); @@ -87,15 +95,6 @@ describe('migrations', () => { type: 't', }) .into('relations'); - await knex - .insert({ - entity_id: 'i1', - hash: 'h', - stitch_ticket: '', - final_entity: '{}', - entity_ref: 'k:ns/n1', - }) - .into('final_entities'); await knex.delete().from('refresh_state').where({ entity_id: 'i1' }); @@ -690,4 +689,407 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20260214000000_search_fk_final_entities.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + // Run migrations up to just before the target migration + await migrateUntilBefore( + knex, + '20260214000000_search_fk_final_entities.js', + ); + + // Insert rows into refresh_state + await knex('refresh_state').insert([ + { + entity_id: 'id1', + entity_ref: 'component:default/service-a', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + { + entity_id: 'id2', + entity_ref: 'component:default/service-b', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + { + entity_id: 'id3', + entity_ref: 'api:default/my-api', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + + // Insert rows into final_entities (only id1 and id2, not id3 - to test orphan deletion) + await knex('final_entities').insert([ + { + entity_id: 'id1', + entity_ref: 'component:default/service-a', + hash: 'hash1', + stitch_ticket: 'ticket1', + final_entity: '{}', + }, + { + entity_id: 'id2', + entity_ref: 'component:default/service-b', + hash: 'hash2', + stitch_ticket: 'ticket2', + final_entity: '{}', + }, + ]); + + // Insert rows into search table (with entity_id FK to refresh_state before migration) + await knex('search').insert([ + { + entity_id: 'id1', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id1', + key: 'metadata.name', + value: 'service-a', + original_value: 'service-a', + }, + { + entity_id: 'id2', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id2', + key: 'metadata.name', + value: 'service-b', + original_value: 'service-b', + }, + // id3 has no final_entities row, so this should be deleted during migration + { entity_id: 'id3', key: 'kind', value: 'api', original_value: 'API' }, + { + entity_id: 'id3', + key: 'metadata.name', + value: 'my-api', + original_value: 'my-api', + }, + // NULL entity_id row should survive the migration + { + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', + }, + ]); + + // Verify initial state + const preMigrationCount = await knex('search').count('* as count'); + expect(Number(preMigrationCount[0].count)).toBe(7); + + // Run the migration + await migrateUpOnce(knex); + + // Verify orphaned rows (id3) were deleted, but NULL entity_id row survived + const postMigrationRows = await knex('search'); + expect(postMigrationRows).toHaveLength(5); + expect(postMigrationRows).toEqual( + expect.arrayContaining([ + { + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', + }, + { + entity_id: 'id1', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id1', + key: 'metadata.name', + value: 'service-a', + original_value: 'service-a', + }, + { + entity_id: 'id2', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id2', + key: 'metadata.name', + value: 'service-b', + original_value: 'service-b', + }, + ]), + ); + + // Verify FK is enforced: inserting with a nonexistent entity_id should be rejected + await expect( + knex('search').insert({ + entity_id: 'nonexistent', + key: 'kind', + value: 'test', + original_value: 'test', + }), + ).rejects.toEqual(expect.anything()); + + // Verify FK cascade works: deleting from final_entities should cascade to search + await knex('final_entities').where({ entity_id: 'id1' }).delete(); + const searchAfterDelete = await knex('search'); + expect(searchAfterDelete).toHaveLength(3); + expect(searchAfterDelete).toEqual( + expect.arrayContaining([ + { + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', + }, + { + entity_id: 'id2', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id2', + key: 'metadata.name', + value: 'service-b', + original_value: 'service-b', + }, + ]), + ); + + // Restore id1 for down migration test + await knex('final_entities').insert({ + entity_id: 'id1', + entity_ref: 'component:default/service-a', + hash: 'hash1', + stitch_ticket: 'ticket1', + final_entity: '{}', + }); + await knex('search').insert([ + { + entity_id: 'id1', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + ]); + + // Delete id1 from refresh_state so it becomes an orphan for the down + // migration (exists in final_entities but not in refresh_state) + await knex('refresh_state').where({ entity_id: 'id1' }).delete(); + + // Run the down migration + await migrateDownOnce(knex); + + // Verify id1 search rows were cleaned up (orphan for refresh_state FK), + // while id2 and the NULL entity_id row survived + const revertedSearchRows = await knex('search'); + expect(revertedSearchRows).toHaveLength(3); + expect(revertedSearchRows).toEqual( + expect.arrayContaining([ + { + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', + }, + { + entity_id: 'id2', + key: 'kind', + value: 'component', + original_value: 'Component', + }, + { + entity_id: 'id2', + key: 'metadata.name', + value: 'service-b', + original_value: 'service-b', + }, + ]), + ); + + // Verify FK is back to refresh_state: deleting from refresh_state should cascade + await knex('refresh_state').where({ entity_id: 'id2' }).delete(); + const afterRefreshStateDelete = await knex('search'); + expect(afterRefreshStateDelete).toEqual([ + { + entity_id: null, + key: 'global', + value: 'setting', + original_value: 'setting', + }, + ]); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + '20260215000000_move_stitch_queue.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + // Run migrations up to just before the target migration + await migrateUntilBefore(knex, '20260215000000_move_stitch_queue.js'); + + // Insert rows into refresh_state with stitch queue data + // Before migration, refresh_state has next_stitch_at and next_stitch_ticket columns + const stitchTime1 = new Date('2026-01-15T12:00:00.000Z'); + const stitchTime3 = new Date('2026-01-16T12:00:00.000Z'); + + await knex('refresh_state').insert([ + { + entity_id: 'id1', + entity_ref: 'component:default/with-stitch', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: stitchTime1, + next_stitch_ticket: 'ticket-1', + }, + { + entity_id: 'id2', + entity_ref: 'component:default/no-stitch', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: null, + next_stitch_ticket: null, + }, + { + entity_id: 'id3', + entity_ref: 'component:default/orphan-stitch', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: stitchTime3, + next_stitch_ticket: 'ticket-3', + }, + ]); + + // Insert final_entities rows (with stitch_ticket column that will be dropped) + await knex('final_entities').insert([ + { + entity_id: 'id1', + entity_ref: 'component:default/with-stitch', + hash: 'h1', + stitch_ticket: 'old-ticket-1', + }, + { + entity_id: 'id2', + entity_ref: 'component:default/no-stitch', + hash: 'h2', + stitch_ticket: 'old-ticket-2', + }, + ]); + + // Verify initial state - stitch_queue table should NOT exist yet + const preTableExists = await knex.schema.hasTable('stitch_queue'); + expect(preTableExists).toBe(false); + + // Run the migration + await migrateUpOnce(knex); + + // Verify stitch_queue table was created + const postTableExists = await knex.schema.hasTable('stitch_queue'); + expect(postTableExists).toBe(true); + + // Verify next_stitch_at and next_stitch_ticket columns were removed from refresh_state + const refreshStateColumnInfo = await knex('refresh_state').columnInfo(); + expect(refreshStateColumnInfo.next_stitch_at).toBeUndefined(); + expect(refreshStateColumnInfo.next_stitch_ticket).toBeUndefined(); + + // Verify stitch_ticket column was removed from final_entities + const finalEntitiesColumnInfo = await knex('final_entities').columnInfo(); + expect(finalEntitiesColumnInfo.stitch_ticket).toBeUndefined(); + + // Verify data was migrated correctly to stitch_queue + const stitchQueueAfterUp = await knex('stitch_queue') + .orderBy('entity_ref') + .select('entity_ref', 'stitch_ticket', 'next_stitch_at'); + + // Only entities with pending stitches should be in stitch_queue (id1 and id3) + expect(stitchQueueAfterUp).toEqual([ + { + entity_ref: 'component:default/orphan-stitch', + stitch_ticket: 'ticket-3', + next_stitch_at: expect.anything(), + }, + { + entity_ref: 'component:default/with-stitch', + stitch_ticket: 'ticket-1', + next_stitch_at: expect.anything(), + }, + ]); + + // Run the down migration + await migrateDownOnce(knex); + + // Verify stitch_queue table was dropped + const revertedTableExists = await knex.schema.hasTable('stitch_queue'); + expect(revertedTableExists).toBe(false); + + // Verify stitch_ticket column was restored to final_entities + const revertedFinalEntitiesColumnInfo = await knex( + 'final_entities', + ).columnInfo(); + expect(revertedFinalEntitiesColumnInfo.stitch_ticket).not.toBeUndefined(); + + // Verify next_stitch_at and next_stitch_ticket columns were restored to refresh_state + const revertedRefreshColumnInfo = await knex( + 'refresh_state', + ).columnInfo(); + expect(revertedRefreshColumnInfo.next_stitch_at).not.toBeUndefined(); + expect(revertedRefreshColumnInfo.next_stitch_ticket).not.toBeUndefined(); + + // Verify data was migrated back to refresh_state + const refreshStateAfterDown = await knex('refresh_state') + .orderBy('entity_id') + .select( + 'entity_id', + 'entity_ref', + 'next_stitch_at', + 'next_stitch_ticket', + ); + + // id1 should have stitch data restored + const id1RefreshRow = refreshStateAfterDown.find( + r => r.entity_id === 'id1', + ); + expect(id1RefreshRow?.next_stitch_at).not.toBeNull(); + expect(id1RefreshRow?.next_stitch_ticket).toBe('ticket-1'); + + // id2 should have no stitch data + const id2RefreshRow = refreshStateAfterDown.find( + r => r.entity_id === 'id2', + ); + expect(id2RefreshRow?.next_stitch_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts b/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts index 6efa3abbc7..53991e4416 100644 --- a/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts +++ b/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts @@ -19,6 +19,7 @@ import { Knex } from 'knex'; import { DefaultProcessingDatabase } from '../../database/DefaultProcessingDatabase'; import { applyDatabaseMigrations } from '../../database/migrations'; import { describePerformanceTest, performanceTraceEnabled } from './lib/env'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; // #region Helpers @@ -81,6 +82,7 @@ describePerformanceTest('getProcessableEntities', () => { logger: mockServices.logger.mock(), events: mockServices.events.mock(), refreshInterval: () => 0, + metrics: metricsServiceMock.mock(), }); const start = Date.now(); diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 7e80825934..19ca5458ed 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-common +## 1.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + ## 1.1.8-next.0 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 1161ec089c..283619f739 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.1.8-next.0", + "version": "1.1.8", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 62f20cec83..ffb1d87c6a 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-catalog-graph +## 0.6.0-next.2 + +### Minor Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + +## 0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## 0.5.7 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 8dd27c4: Fix large icon rendering in catalog graph nodes +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.5.7-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/README-alpha.md b/plugins/catalog-graph/README-alpha.md index 6242e9cc39..90481caca6 100644 --- a/plugins/catalog-graph/README-alpha.md +++ b/plugins/catalog-graph/README-alpha.md @@ -1,8 +1,6 @@ -# Catalog Graph +# Catalog Graph - Extension Reference -> [!WARNING] -> This documentation is made for those using the experimental new Frontend system. -> If you are not using the new frontend system, please go [here](./README.md). +This page contains detailed documentation for all extensions provided by the `@backstage/plugin-catalog-graph` plugin. For general information about the plugin, see the [README](./README.md). The Catalog graph plugin helps you to visualize the relations between entities, like ownership, grouping or API relationships. It comes with these features: diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index b411d4d868..058fc28164 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -1,8 +1,5 @@ # catalog-graph -> Disclaimer: -> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). - Welcome to the catalog graph plugin! The catalog graph visualizes the relations between entities, like ownership, grouping or API relationships. @@ -23,58 +20,25 @@ The plugin comes with these features: - `EntityRelationsGraph`: A react component that can be used to build own customized entity relation graphs. -## Usage +## Installation -To use the catalog graph plugin, you have to add some things to your Backstage app: +```sh +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-catalog-graph +``` -1. Add a dependency to your `packages/app/package.json`: - ```sh - # From your Backstage root directory - yarn --cwd packages/app add @backstage/plugin-catalog-graph - ``` -2. Add the `CatalogGraphPage` to your `packages/app/src/App.tsx`: +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). - ```typescript - - … - } />… - - ``` +To enable the entity relations graph card on the catalog entity page, add the following configuration: - You can configure the page to open with some initial filters: +```yaml +# app-config.yaml +app: + extensions: + - entity-card:catalog-graph/relations +``` - ```typescript - - } - /> - ``` - -3. Bind the external routes of the `catalogGraphPlugin` in your `packages/app/src/App.tsx`: - - ```typescript - bindRoutes({ bind }) { - … - bind(catalogGraphPlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - … - } - ``` - -4. Add `EntityCatalogGraphCard` to any entity page that you want in your `packages/app/src/components/catalog/EntityPage.tsx`: - - ```typescript - - - - ``` +For the full list of available extensions and their configuration options, see the [README-alpha.md](./README-alpha.md). ### Customizing the UI @@ -177,6 +141,54 @@ import { }), ``` +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + +1. Add the `CatalogGraphPage` to your `packages/app/src/App.tsx`: + + ```typescript + + … + } />… + + ``` + + You can configure the page to open with some initial filters: + + ```typescript + + } + /> + ``` + +2. Bind the external routes of the `catalogGraphPlugin` in your `packages/app/src/App.tsx`: + + ```typescript + bindRoutes({ bind }) { + … + bind(catalogGraphPlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + … + } + ``` + +3. Add `EntityCatalogGraphCard` to any entity page that you want in your `packages/app/src/components/catalog/EntityPage.tsx`: + + ```typescript + + + + ``` + ## Development Run `yarn` in the root of this plugin to install all dependencies and then `yarn start` to run a [development version](./dev/index.tsx) of this plugin. diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 125f4d7ecb..dc69ce2122 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.5.7-next.2", + "version": "0.6.0-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", @@ -56,9 +56,11 @@ "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/types": "workspace:^", + "@backstage/ui": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@remixicon/react": "^4.6.0", "classnames": "^2.3.1", "lodash": "^4.17.15", "p-limit": "^3.1.0", diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index 826155e87a..f9d4542625 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -6,12 +6,15 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FilterPredicate } from '@backstage/filter-predicates'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -157,6 +160,7 @@ const _default: OverridableFrontendPlugin< relationPairs: [string, string][] | undefined; zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; path: string | undefined; + title: string | undefined; }; configInput: { curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; @@ -172,26 +176,74 @@ const _default: OverridableFrontendPlugin< selectedRelations?: string[] | undefined; selectedKinds?: string[] | undefined; showFilters?: boolean | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 8d3b8f2cc8..74b9713cf2 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -9,7 +9,6 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; -import { InfoCardVariants } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { MouseEvent as MouseEvent_2 } from 'react'; @@ -138,7 +137,6 @@ export namespace Direction { // @public export const EntityCatalogGraphCard: ( props: Partial & { - variant?: InfoCardVariants; height?: number; title?: string; action?: ReactNode; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b08404f63b..20b4074f79 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -19,13 +19,15 @@ import { parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; -import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { + EntityInfoCard, humanizeEntityRef, useEntity, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { ButtonLink } from '@backstage/ui'; +import { RiArrowRightLine } from '@remixicon/react'; import { makeStyles, Theme } from '@material-ui/core/styles'; import qs from 'qs'; import { MouseEvent, ReactNode, useCallback, useMemo } from 'react'; @@ -41,23 +43,13 @@ import { Direction, EntityNode } from '../../lib/types'; import classNames from 'classnames'; /** @public */ -export type CatalogGraphCardClassKey = 'card' | 'graph'; +export type CatalogGraphCardClassKey = 'graph'; const useStyles = makeStyles( { - card: ({ height }) => ({ - display: 'flex', - flexDirection: 'column', - ...(height && { - height, - maxHeight: height, - minHeight: height, - }), - }), graph: ({ height }) => ({ - flex: height ? '0 0 auto' : 1, + height: height ?? '100%', minHeight: 0, - ...(height && { height }), }), }, { name: 'PluginCatalogGraphCatalogGraphCard' }, @@ -65,7 +57,6 @@ const useStyles = makeStyles( export const CatalogGraphCard = ( props: Partial & { - variant?: InfoCardVariants; height?: number; title?: string; action?: ReactNode; @@ -73,7 +64,6 @@ export const CatalogGraphCard = ( ) => { const { t } = useTranslationRef(catalogGraphTranslationRef); const { - variant = 'gridItem', relationPairs, maxDepth = 1, unidirectional = true, @@ -132,16 +122,18 @@ export const CatalogGraphCard = ( const catalogGraphUrl = `${catalogGraphRoute()}${catalogGraphParams}`; return ( - } + variant="tertiary" + href={catalogGraphUrl} + > + {t('catalogGraphCard.deepLinkTitle')} + + } > - + ); }; diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 7626787246..d99287bac3 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-catalog-import +## 0.13.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 0.13.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.13.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.13.10 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.13.10-next.2 ### Patch Changes diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md index d4ede8af7b..efb7c84fa3 100644 --- a/plugins/catalog-import/README.md +++ b/plugins/catalog-import/README.md @@ -15,22 +15,14 @@ Some features are not yet available for all supported Git providers. ## Getting Started -1. Install the Catalog Import Plugin: +Install the Catalog Import Plugin: ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-catalog-import ``` -2. Add the `CatalogImportPage` extension to the app: - -```tsx -// packages/app/src/App.tsx - -import { CatalogImportPage } from '@backstage/plugin-catalog-import'; - -} />; -``` +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). ## Customizations @@ -104,6 +96,22 @@ Following React components accept optional props for providing custom example en /> ``` +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the +plugin into your app as outlined in this section. If you are on the new frontend +system, you can skip this. + +Add the `CatalogImportPage` extension to the app: + +```tsx +// packages/app/src/App.tsx + +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; + +} />; +``` + ## Development Use `yarn start` to run a [development version](./dev/index.tsx) of the plugin that can be used to validate each flow with mocked data. diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index a7fbce946d..21ec443d00 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.13.10-next.2", + "version": "0.13.11-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index 85c412f84d..b58b641050 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -6,8 +6,11 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -115,26 +118,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 6531f4b9fc..abb46b7325 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,80 @@ # @backstage/plugin-catalog-node +## 2.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 2.1.0-next.0 + +### Minor Changes + +- bf71677: Added the ability for SCM events subscribers to mark the fact that they have taken actions based on events, which produces output metrics: + + - `catalog.events.scm.actions` with attribute `action`: Counter for the number of actions actually taken by catalog internals or other subscribers, based on SCM events. The `action` is currently either `create`, `delete`, `refresh`, or `move`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 2.0.0 + +### Minor Changes + +- cfd8103: Promoted stable catalog extension points from alpha to main export. The following extension points are now exported from `@backstage/plugin-catalog-node` instead of `@backstage/plugin-catalog-node/alpha`: + + - `catalogLocationsExtensionPoint` and `CatalogLocationsExtensionPoint` + - `catalogProcessingExtensionPoint` and `CatalogProcessingExtensionPoint` + - `catalogAnalysisExtensionPoint` and `CatalogAnalysisExtensionPoint` + + The old alpha exports for these extension points are now deprecated with `@deprecated` markers pointing to the new stable exports. Please update your imports from `@backstage/plugin-catalog-node/alpha` to `@backstage/plugin-catalog-node`. + + Note: The `catalogModelExtensionPoint`, `catalogPermissionExtensionPoint`, and related types remain in alpha. + +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries +- 34cc520: Introduced the `catalogScmEventsServiceRef`, along with `CatalogScmEventsService` and associated types. These allow communicating a unified set of events, that parts of the catalog can react to. + +### Patch Changes + +- 42abfb1: Updated `catalogServiceMock.mock` to use `createServiceMock` from `@backstage/backend-test-utils`, replacing the internal copy of `simpleMock`. Added `@backstage/backend-test-utils` as an optional peer dependency. +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-test-utils@1.11.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-catalog-common@1.1.8 + ## 2.0.0-next.1 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 50c16d8c4c..a39dd1f5d9 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "2.0.0-next.1", + "version": "2.1.0-next.2", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", @@ -68,6 +68,7 @@ "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@backstage/types": "workspace:^", + "@opentelemetry/api": "^1.9.0", "lodash": "^4.17.21", "yaml": "^2.0.0" }, diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index aad7518921..0598834c67 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -125,6 +125,7 @@ export type CatalogScmEventContext = { // @alpha export interface CatalogScmEventsService { + markEventActionTaken(options: { count?: number; action: string }): void; publish(events: CatalogScmEvent[]): Promise; subscribe(subscriber: CatalogScmEventsServiceSubscriber): { unsubscribe: () => void; diff --git a/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.test.ts b/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.test.ts index 4adbb3b736..91a21894c8 100644 --- a/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.test.ts +++ b/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.test.ts @@ -15,11 +15,21 @@ */ import { createDeferred } from '@backstage/types'; +import { MetricsAPI } from '@opentelemetry/api'; import { DefaultCatalogScmEventsService } from './DefaultCatalogScmEventsService'; describe('DefaultCatalogScmEventsService', () => { + const counterAdd = jest.fn(); + const mockMetrics = { + getMeter: () => ({ + createCounter: () => ({ + add: counterAdd, + }), + }), + } as unknown as MetricsAPI; + it('should publish and subscribe to events', async () => { - const service = new DefaultCatalogScmEventsService(); + const service = new DefaultCatalogScmEventsService(mockMetrics); const subscriber1 = { onEvents: jest.fn(), @@ -53,7 +63,7 @@ describe('DefaultCatalogScmEventsService', () => { }); it('waits for all subscribers to acknowledge the events', async () => { - const service = new DefaultCatalogScmEventsService(); + const service = new DefaultCatalogScmEventsService(mockMetrics); const work1 = createDeferred(); const work2 = createDeferred(); @@ -102,4 +112,12 @@ describe('DefaultCatalogScmEventsService', () => { expect(completed).toBe(true); }); + + it('marks event actions taken', () => { + const service = new DefaultCatalogScmEventsService(mockMetrics); + + service.markEventActionTaken({ action: 'refresh' }); + + expect(counterAdd).toHaveBeenCalledWith(1, { action: 'refresh' }); + }); }); diff --git a/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.ts b/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.ts index 7610b7fd0a..b61878bbae 100644 --- a/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.ts +++ b/plugins/catalog-node/src/scmEvents/DefaultCatalogScmEventsService.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Counter, MetricsAPI } from '@opentelemetry/api'; import { CatalogScmEvent, CatalogScmEventsService, @@ -21,19 +22,36 @@ import { } from './types'; /** - * The default implementation of the {@link CatalogScmEventsService}/{@link catalogScmEventsServiceRef}. + * The default implementation of the + * {@link CatalogScmEventsService}/{@link catalogScmEventsServiceRef}. * * @internal * @remarks * * This implementation is in-memory, which requires the producers and consumer * (the catalog backend) to be deployed together. + * + * It's defined in here instead of in the catalog-backend plugin because this + * allows us to have a default factory whether you happen to be co-installed + * with the catalog-backend plugin or not. */ export class DefaultCatalogScmEventsService implements CatalogScmEventsService { readonly #subscribers: Set; + readonly #metrics: { + actions: Counter<{ action: string }>; + }; - constructor() { + constructor(metrics: MetricsAPI) { this.#subscribers = new Set(); + + const meter = metrics.getMeter('default'); + this.#metrics = { + actions: meter.createCounter('catalog.events.scm.actions', { + description: + 'Number of actions taken as a result of SCM event messages', + unit: 'short', + }), + }; } subscribe(subscriber: CatalogScmEventsServiceSubscriber): { @@ -58,4 +76,8 @@ export class DefaultCatalogScmEventsService implements CatalogScmEventsService { }), ); } + + markEventActionTaken(options: { count?: number; action: string }): void { + this.#metrics.actions.add(options.count ?? 1, { action: options.action }); + } } diff --git a/plugins/catalog-node/src/scmEvents/catalogScmEventsServiceRef.ts b/plugins/catalog-node/src/scmEvents/catalogScmEventsServiceRef.ts index 1f5bc7165f..f940581ca0 100644 --- a/plugins/catalog-node/src/scmEvents/catalogScmEventsServiceRef.ts +++ b/plugins/catalog-node/src/scmEvents/catalogScmEventsServiceRef.ts @@ -18,6 +18,7 @@ import { createServiceFactory, createServiceRef, } from '@backstage/backend-plugin-api'; +import { metrics } from '@opentelemetry/api'; import { CatalogScmEventsService } from './types'; import { DefaultCatalogScmEventsService } from './DefaultCatalogScmEventsService'; @@ -39,7 +40,7 @@ export const catalogScmEventsServiceRef = service, deps: {}, createRootContext() { - return new DefaultCatalogScmEventsService(); + return new DefaultCatalogScmEventsService(metrics); }, factory(_, ctx) { return ctx; diff --git a/plugins/catalog-node/src/scmEvents/types.ts b/plugins/catalog-node/src/scmEvents/types.ts index 87cf31fc01..72161cacde 100644 --- a/plugins/catalog-node/src/scmEvents/types.ts +++ b/plugins/catalog-node/src/scmEvents/types.ts @@ -55,6 +55,26 @@ export interface CatalogScmEventsService { * guarantees. */ publish(events: CatalogScmEvent[]): Promise; + + /** + * As a consumer of SCM events, mark that you have taken an action as a result + * of an SCM event. + * + * This is typically used to record metrics or other observability signals + * about how SCM events are handled, for example counting how many refresh, + * delete, create, or move operations are triggered by incoming events. + */ + markEventActionTaken(options: { + /** + * The number of actions taken of the given type. Defaults to 1. + */ + count?: number; + /** + * The type of action taken - typically "refresh", "delete", + * "create", or "move". + */ + action: string; + }): void; } /** diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 4d2c92760b..4603c1b31e 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,125 @@ # @backstage/plugin-catalog-react +## 2.1.0-next.2 + +### Minor Changes + +- d14b6e0: Exported `useEntityRefLink` hook that returns a function for generating entity page URLs from entity references. +- c6080eb: Added `EntityInfoCard` component to `@backstage/plugin-catalog-react` as a BUI-based card wrapper for entity page cards. + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-test-utils@0.5.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 2.1.0-next.1 + +### Minor Changes + +- 4d58894: Added `aliases` and `contentOrder` fields to `EntityContentGroupDefinition`, allowing groups to declare alias IDs and control the sort order of their content items. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 2.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/frontend-test-utils@0.5.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 2.0.0 + +### Minor Changes + +- 0e9578d: Migrated `UnregisterEntityDialog` from Material UI to Backstage UI components. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- b4e8249: Implemented support for the new `queryLocations` and `streamLocations` that allow paginated/streamed and filtered location queries +- 7feb83b: **BREAKING ALPHA**: All of the predicate types and functions have been moved to the `@backstage/filter-predicates` package. + + When moving into the more general package, they were renamed as follows: + + - `EntityPredicate` -> `FilterPredicate` + - `EntityPredicateExpression` -> `FilterPredicateExpression` + - `EntityPredicatePrimitive` -> `FilterPredicatePrimitive` + - `entityPredicateToFilterFunction` -> `filterPredicateToFilterFunction` + - `EntityPredicateValue` -> `FilterPredicateValue` + +- e8258d0: **BREAKING**: Removed the 'summary' entity card type from `EntityCardType`. Users should migrate to using 'content' or 'info' card types instead. + + TypeScript will now show errors if you try to use `type: 'summary'` when creating entity cards. + +- ac9bead: Added `createTestEntityPage` test utility for testing entity cards and content extensions in the new frontend system. This utility creates a test page extension that provides `EntityProvider` context and accepts entity extensions through input redirects: + + ```typescript + import { renderTestApp } from '@backstage/frontend-test-utils'; + import { createTestEntityPage } from '@backstage/plugin-catalog-react/testUtils'; + + renderTestApp({ + extensions: [createTestEntityPage({ entity: myEntity }), myEntityCard], + }); + ``` + +### Patch Changes + +- f523983: Fixes a bug where the `EntityListProvider` would not correctly hydrate query parameters if more than 20 were provided for the same key. +- 09a6aad: The `catalogApiMock` test utility now returns a `MockWithApiFactory`, allowing it to be passed directly to test utilities like `renderTestApp` and `TestApiProvider` without needing the `[catalogApiRef, catalogApiMock()]` tuple. +- 88dbd5e: fixed bug in `UserListPicker` by getting the `kindParamater` from the `filters` rather than from the `queryParameters` +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/frontend-test-utils@0.5.0 + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + ## 2.0.0-next.2 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index da99159a17..35a0abd20b 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "2.0.0-next.2", + "version": "2.1.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 04c900ee41..9c76a221dc 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -258,7 +258,6 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', @@ -266,6 +265,7 @@ export const EntityContentBlueprint: ExtensionBlueprint<{ optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -345,6 +345,8 @@ export type EntityContentGroupDefinitions = Record< { title: string; icon?: string | ReactElement; + aliases?: string[]; + contentOrder?: 'title' | 'natural'; } >; @@ -556,9 +558,9 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{ }>; // @alpha (undocumented) -export const EntityTableColumnTitle: ({ - translationKey, -}: EntityTableColumnTitleProps) => +export const EntityTableColumnTitle: ( + input: EntityTableColumnTitleProps, +) => | 'System' | 'Title' | 'Domain' diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 14c832d545..3ae36ce941 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -277,6 +277,23 @@ export type EntityFilter = { toQueryValue?: () => string | string[]; }; +// @public (undocumented) +export function EntityInfoCard(props: EntityInfoCardProps): JSX_2.Element; + +// @public (undocumented) +export interface EntityInfoCardProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + footerActions?: ReactNode; + // (undocumented) + headerActions?: ReactNode; + // (undocumented) + title?: ReactNode; +} + // @public export class EntityKindFilter implements EntityFilter { constructor(value: string, label: string); @@ -820,6 +837,11 @@ export function useEntityPresentation( }, ): EntityRefPresentationSnapshot; +// @public +export function useEntityRefLink(): ( + entityRef: Entity | CompoundEntityRef | string, +) => string; + // @public export function useEntityTypeFilter(): { loading: boolean; diff --git a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx index 83b9aa983b..04c13db484 100644 --- a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx @@ -47,6 +47,10 @@ export type EntityContentGroupDefinitions = Record< { title: string; icon?: string | ReactElement; + /** Other group IDs that should be treated as aliases for this group. */ + aliases?: string[]; + /** How to sort the content items within this group. Overrides the page-level default. */ + contentOrder?: 'title' | 'natural'; } >; diff --git a/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx new file mode 100644 index 0000000000..b3b0efd596 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2026 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 { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import { EntityInfoCard } from './EntityInfoCard'; + +describe('', () => { + it('renders children in the card body', async () => { + await renderInTestApp( + +
    Body content
    +
    , + ); + + expect(screen.getByText('Body content')).toBeInTheDocument(); + }); + + it('renders the title when provided', async () => { + await renderInTestApp( + +
    Body
    +
    , + ); + + expect( + screen.getByRole('heading', { name: 'My Card Title' }), + ).toBeInTheDocument(); + }); + + it('does not render a heading when title is not provided', async () => { + await renderInTestApp( + +
    Body
    +
    , + ); + + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + }); + + it('renders header actions next to the title', async () => { + await renderInTestApp( + Edit}> +
    Body
    +
    , + ); + + expect(screen.getByRole('heading', { name: 'Title' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); + }); + + it('renders footer actions when provided', async () => { + await renderInTestApp( + Next Page}> +
    Body
    +
    , + ); + + expect( + screen.getByRole('button', { name: 'Next Page' }), + ).toBeInTheDocument(); + }); + + it('does not render footer when footerActions is not provided', async () => { + const { container } = await renderInTestApp( + +
    Body
    +
    , + ); + + expect(container.querySelector('.bui-CardFooter')).not.toBeInTheDocument(); + }); + + it('renders JSX titles (e.g., icon + text)', async () => { + await renderInTestApp( + + 🏠 Home Card + + } + > +
    Body
    +
    , + ); + + expect(screen.getByText('Home Card')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx new file mode 100644 index 0000000000..54db900fa4 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2026 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 { ReactNode } from 'react'; +import { + Card, + CardHeader, + CardBody, + CardFooter, + Text, + Flex, +} from '@backstage/ui'; +import { makeStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; + +const useStyles = makeStyles({ + root: { + height: '100%', + }, + footer: { + display: 'flex', + justifyContent: 'flex-end', + }, +}); + +/** @public */ +export interface EntityInfoCardProps { + title?: ReactNode; + headerActions?: ReactNode; + footerActions?: ReactNode; + children?: ReactNode; + className?: string; +} + +/** @public */ +export function EntityInfoCard(props: EntityInfoCardProps) { + const { title, headerActions, footerActions, children, className } = props; + const classes = useStyles(); + + return ( + + {title && ( + + + + {title} + + {headerActions && ( + + {headerActions} + + )} + + + )} + {children} + {footerActions && ( + {footerActions} + )} + + ); +} diff --git a/plugins/catalog-react/src/components/EntityInfoCard/index.ts b/plugins/catalog-react/src/components/EntityInfoCard/index.ts new file mode 100644 index 0000000000..1999a228b1 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { EntityInfoCard } from './EntityInfoCard'; +export type { EntityInfoCardProps } from './EntityInfoCard'; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 40c738a66e..8af051db8c 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -17,7 +17,7 @@ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; -import { ReactNode, forwardRef } from 'react'; +import { ReactNode, forwardRef, useCallback } from 'react'; import { entityRouteParams, entityRouteRef } from '../../routes'; import { EntityDisplayName } from '../EntityDisplayName'; @@ -54,7 +54,7 @@ export const EntityRefLink = forwardRef( disableTooltip, ...linkProps } = props; - const entityRoute = useEntityRoute(props.entityRef); + const entityLink = useEntityRefLink(); const content = children ?? title ?? ( ( ); return ( - + {content} ); }, ) as (props: EntityRefLinkProps) => JSX.Element; -// Hook that computes the route to a given entity / ref. This is a bit -// contrived, because it tries to retain the casing of the entity name if -// present, but not of other parts. This is in an attempt to make slightly more -// nice-looking URLs. -function useEntityRoute( +/** + * Returns a function that generates a route path to the given entity. + * + * @public + */ +export function useEntityRefLink(): ( entityRef: Entity | CompoundEntityRef | string, -): string { +) => string { const entityRoute = useRouteRef(entityRouteRef); - const routeParams = entityRouteParams(entityRef, { encodeParams: true }); - - return entityRoute(routeParams); + return useCallback( + (entityRef: Entity | CompoundEntityRef | string) => { + const routeParams = entityRouteParams(entityRef, { encodeParams: true }); + return entityRoute(routeParams); + }, + [entityRoute], + ); } diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index 50ac3d4534..82773da141 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { EntityRefLink } from './EntityRefLink'; +export { EntityRefLink, useEntityRefLink } from './EntityRefLink'; export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; export type { EntityRefLinksProps } from './EntityRefLinks'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 8a1ebd1835..971d6086f7 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -35,3 +35,4 @@ export * from './EntityProcessingStatusPicker'; export * from './EntityNamespacePicker'; export * from './EntityAutocompletePicker'; export * from './MissingAnnotationEmptyState'; +export * from './EntityInfoCard'; diff --git a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md index e76568388a..18acadef24 100644 --- a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-unprocessed-entities-common +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + ## 0.0.13-next.0 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities-common/package.json b/plugins/catalog-unprocessed-entities-common/package.json index 75b668ab9d..cf1e82df1f 100644 --- a/plugins/catalog-unprocessed-entities-common/package.json +++ b/plugins/catalog-unprocessed-entities-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities-common", - "version": "0.0.13-next.0", + "version": "0.0.13", "description": "Common functionalities for the catalog-unprocessed-entities plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 6b0ff99466..ee8ba2f3a6 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-devtools-react@0.1.2-next.1 + +## 0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-devtools-react@0.1.2-next.0 + +## 0.2.26 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-devtools-react@0.1.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + ## 0.2.26-next.1 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/README.md b/plugins/catalog-unprocessed-entities/README.md index b39a4a7ad4..fc20168b80 100644 --- a/plugins/catalog-unprocessed-entities/README.md +++ b/plugins/catalog-unprocessed-entities/README.md @@ -32,7 +32,26 @@ Requires the `@backstage/plugin-catalog-backend-module-unprocessed` module to be yarn --cwd packages/app add @backstage/plugin-catalog-unprocessed-entities ``` -Import into your `App.tsx` and include into the `` component: +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). + +You can optionally add unprocessed entities as a tab in DevTools through configuration: + +```yaml +app: + extensions: + # Enable the catalog-unprocessed-entities tab in devtools + - devtools-content:catalog-unprocessed-entities: true + # Disable the catalog-unprocessed-entities element outside devtools including the sidebar + - page:catalog-unprocessed-entities: false +``` + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the +plugin into your app as outlined in this section. If you are on the new frontend +system, you can skip this. + +Import it into your `App.tsx` and include it in the `` component: ```tsx title="packages/app/src/App.tsx" import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities'; @@ -44,44 +63,6 @@ import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unproc />; ``` -### Integrating with the New Frontend System - -Follow this section if you are using Backstage's [new frontend system](https://backstage.io/docs/frontend-system/). - -Import `catalogUnprocessedEntitiesPlugin` in your `App.tsx` and add it to your app's `features` array: - -```typescript -import catalogUnprocessedEntitiesPlugin from '@backstage/plugin-catalog-unprocessed-entities'; -import { unprocessedEntitiesDevToolsContent } from '@backstage/plugin-catalog-unprocessed-entities/alpha'; - -// Optionally add unprocessed entities route to devtools -const devtoolsPluginUnprocessed = createFrontendModule({ - pluginId: 'catalog-unprocessed-entities', - extensions: [unprocessedEntitiesDevToolsContent], -}); - -export const app = createApp({ - features: [ - // ... - catalogUnprocessedEntitiesPlugin, - - // Optionally add unprocessed entities route to devtools - devtoolsPluginUnprocessed, - devtoolsPlugin, // devtools plugin needs to be added too, if autodiscover is disabled - // ... - ], -}); -``` - -```yaml -app: - extensions: - # Enable the catalog-unprocessed-entities tab in devtools - - devtools-content:catalog-unprocessed-entities: true - # Disable the catalog-unprocessed-entities element outside devtools including the sidebar - - page:catalog-unprocessed-entities: false -``` - ## Customization If you want to use the provided endpoints in a different way, you can use the ApiRef doing the following: diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 6eaaba7ab9..eea96e391e 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.26-next.1", + "version": "0.2.27-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog-unprocessed-entities/report-alpha.api.md b/plugins/catalog-unprocessed-entities/report-alpha.api.md index d3bd6dd28f..7408831f87 100644 --- a/plugins/catalog-unprocessed-entities/report-alpha.api.md +++ b/plugins/catalog-unprocessed-entities/report-alpha.api.md @@ -6,10 +6,13 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { DevToolsContentBlueprintParams } from '@backstage/plugin-devtools-react'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -64,26 +67,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } @@ -104,7 +156,6 @@ export const unprocessedEntitiesDevToolsContent: OverridableExtensionDefinition< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -112,6 +163,7 @@ export const unprocessedEntitiesDevToolsContent: OverridableExtensionDefinition< optional: true; } > + | ExtensionDataRef | ExtensionDataRef; inputs: {}; params: DevToolsContentBlueprintParams; diff --git a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx index 5ccae907aa..d2987f3197 100644 --- a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx +++ b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx @@ -68,6 +68,8 @@ export const catalogUnprocessedEntitiesNavItem = NavItemBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'catalog-unprocessed-entities', + title: 'Unprocessed Entities', + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 9f426db4f5..ff10fcb670 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,136 @@ # @backstage/plugin-catalog +## 2.0.0-next.2 + +### Major Changes + +- 5fc35bb: Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + + **BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + + **Migration:** + + Simply delete the obsolete `variant` and `gridSizes` props, e.g: + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 1.34.0-next.1 + +### Minor Changes + +- 4d58894: Added support for group alias IDs and configurable content ordering on the entity page. Groups can now declare `aliases` so that content targeting an aliased group is included in the group. A new `defaultContentOrder` option (default `title`) controls how content items within each group are sorted, with support for both a page-level default and per-group overrides. + +### Patch Changes + +- 07ba746: Fixed entity page tab groups not respecting the ordering from the `groups` configuration. +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.33.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.33.0 + +### Minor Changes + +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 05aac34: Migrated `DeleteEntityDialog` and `EntityOrphanWarning` components to Backstage UI. + + The `deleteEntity.description` translation key no longer includes "Click here to delete" text. A new `deleteEntity.actionButtonTitle` key was added for the action button. + +### Patch Changes + +- 220d6c3: Add missing translation entries for catalog UI text. + + This change adds translation keys and updates relevant UI components to use the correct localized labels and text in the catalog plugin. It ensures that catalog screens such as entity layout, tabs, search result items, table labels, and other UI elements correctly reference the i18n system for translation. + + No functional behavior is changed aside from the improved internationalization support. + +- 8d4c48b: Fixed vertical spacing between tags in the catalog table. +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- e8258d0: The default entity content layout still supports rendering summary cards at runtime for backward compatibility, but logs a console warning when they are detected to help identify where migration is needed. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 75ac651: Migrated `EntityRelationWarning` and `EntityProcessingErrorsPanel` components from Material UI to Backstage UI. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 1.33.0-next.2 ### Minor Changes diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md index e8de1118ac..af8c82b35c 100644 --- a/plugins/catalog/README.md +++ b/plugins/catalog/README.md @@ -15,13 +15,17 @@ To check if you already have the package, look under `@backstage/plugin-catalog`. The instructions below walk through restoring the plugin, if you previously removed it. -### Install the package - ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-catalog ``` +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + ### Add the plugin to your `packages/app` Add the two pages that the catalog plugin provides to your app. You can choose diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 192193b995..835cc9139b 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.33.0-next.2", + "version": "2.0.0-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index d154ed4981..a0b092a453 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -18,6 +18,7 @@ import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { JSXElementConstructor } from 'react'; @@ -95,6 +96,8 @@ export const catalogTranslationRef: TranslationRef< 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'; @@ -756,7 +759,6 @@ const _default: OverridableFrontendPlugin< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -764,6 +766,7 @@ const _default: OverridableFrontendPlugin< optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -998,6 +1001,7 @@ const _default: OverridableFrontendPlugin< limit?: number | undefined; }; path: string | undefined; + title: string | undefined; }; configInput: { pagination?: @@ -1008,19 +1012,64 @@ const _default: OverridableFrontendPlugin< limit?: number | undefined; } | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; filters: ExtensionInput< ConfigurableExtensionDataRef, { @@ -1033,10 +1082,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; 'page:catalog/entity': OverridableExtensionDefinition<{ @@ -1047,11 +1098,15 @@ const _default: OverridableFrontendPlugin< { title: string; icon?: string | undefined; + aliases?: string[] | undefined; + contentOrder?: 'title' | 'natural' | undefined; } >[] | undefined; + defaultContentOrder: 'title' | 'natural'; showNavItemIcons: boolean; path: string | undefined; + title: string | undefined; }; configInput: { groups?: @@ -1060,23 +1115,71 @@ const _default: OverridableFrontendPlugin< { title: string; icon?: string | undefined; + aliases?: string[] | undefined; + contentOrder?: 'title' | 'natural' | undefined; } >[] | undefined; + defaultContentOrder?: 'title' | 'natural' | undefined; showNavItemIcons?: boolean | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; headers: ExtensionInput< | ConfigurableExtensionDataRef< (entity: Entity) => boolean, @@ -1166,10 +1269,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: 'entity'; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; 'search-result-list-item:catalog': OverridableExtensionDefinition<{ diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 52b13e1c43..48e3f53de2 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -40,11 +40,6 @@ import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core/Tab'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// @public -export type AboutCardProps = { - variant?: InfoCardVariants; -}; - // @public (undocumented) export function AboutContent(props: AboutContentProps): JSX_2.Element; @@ -64,8 +59,6 @@ export interface AboutFieldProps { // (undocumented) className?: string; // (undocumented) - gridSizes?: Record; - // (undocumented) label: string; // (undocumented) value?: string; @@ -334,7 +327,7 @@ export interface DependsOnResourcesCardProps { } // @public -export const EntityAboutCard: (props: AboutCardProps) => JSX.Element; +export const EntityAboutCard: () => JSX.Element; // @public (undocumented) export type EntityContextMenuClassKey = 'button'; @@ -384,8 +377,6 @@ export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX_2.Element; export interface EntityLabelsCardProps { // (undocumented) title?: string; - // (undocumented) - variant?: InfoCardVariants; } // @public (undocumented) @@ -435,8 +426,6 @@ export const EntityLinksCard: (props: EntityLinksCardProps) => JSX_2.Element; export interface EntityLinksCardProps { // (undocumented) cols?: ColumnBreakpoints | number; - // (undocumented) - variant?: InfoCardVariants; } // @public (undocumented) diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index ee6e2fc012..895f36150f 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -80,6 +80,7 @@ export interface EntityLayoutProps { */ parentEntityRelations?: string[]; groupDefinitions: EntityContentGroupDefinitions; + defaultContentOrder?: 'title' | 'natural'; showNavItemIcons?: boolean; } @@ -110,6 +111,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { NotFoundComponent, parentEntityRelations, groupDefinitions, + defaultContentOrder, showNavItemIcons, } = props; const { kind } = useRouteRefParams(entityRouteRef); @@ -164,6 +166,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { )} diff --git a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.test.tsx b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.test.tsx index 3bca08288f..a85d795cd8 100644 --- a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.test.tsx +++ b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.test.tsx @@ -16,8 +16,10 @@ import { screen } from '@testing-library/react'; import { useSelectedSubRoute } from './EntityTabs'; +import { EntityTabsList } from './EntityTabsList'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { render } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/frontend-test-utils'; function TestSubRouteHook(props: { subRoutes: Array<{ @@ -37,6 +39,83 @@ function TestSubRouteHook(props: { ); } +describe('EntityTabsList', () => { + it('should render groups in the order defined by groupDefinitions', () => { + const tabs = [ + { id: '/cicd', label: 'CI/CD', path: 'cicd', group: 'cicd' }, + { + id: '/overview', + label: 'Overview', + path: 'overview', + group: 'overview', + }, + { + id: '/techdocs', + label: 'TechDocs', + path: 'techdocs', + group: 'techdocs', + }, + ]; + + const groupDefinitions = { + overview: { title: 'Overview' }, + techdocs: { title: 'TechDocs' }, + cicd: { title: 'CI/CD' }, + }; + + renderInTestApp( + , + ); + + const tabElements = screen.getAllByRole('tab'); + expect(tabElements).toHaveLength(3); + expect(tabElements[0]).toHaveTextContent('Overview'); + expect(tabElements[1]).toHaveTextContent('TechDocs'); + expect(tabElements[2]).toHaveTextContent('CI/CD'); + }); + + it('should place ungrouped tabs after defined groups', () => { + const tabs = [ + { id: '/standalone', label: 'Standalone', path: 'standalone' }, + { + id: '/overview', + label: 'Overview', + path: 'overview', + group: 'overview', + }, + { + id: '/techdocs', + label: 'TechDocs', + path: 'techdocs', + group: 'techdocs', + }, + ]; + + const groupDefinitions = { + overview: { title: 'Overview' }, + techdocs: { title: 'TechDocs' }, + }; + + renderInTestApp( + , + ); + + const tabElements = screen.getAllByRole('tab'); + expect(tabElements).toHaveLength(3); + expect(tabElements[0]).toHaveTextContent('Overview'); + expect(tabElements[1]).toHaveTextContent('TechDocs'); + expect(tabElements[2]).toHaveTextContent('Standalone'); + }); +}); + describe('EntityTabs', () => { const subRoutes = [ { diff --git a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx index ed737266a5..102df76042 100644 --- a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx +++ b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabs.tsx @@ -72,11 +72,12 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { type EntityTabsProps = { routes: SubRoute[]; groupDefinitions: EntityContentGroupDefinitions; + defaultContentOrder?: 'title' | 'natural'; showIcons?: boolean; }; export function EntityTabs(props: EntityTabsProps) { - const { routes, groupDefinitions, showIcons } = props; + const { routes, groupDefinitions, defaultContentOrder, showIcons } = props; const { index, route, element } = useSelectedSubRoute(routes); @@ -107,6 +108,7 @@ export function EntityTabs(props: EntityTabsProps) { selectedIndex={index} showIcons={showIcons} groupDefinitions={groupDefinitions} + defaultContentOrder={defaultContentOrder} /> diff --git a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsList.tsx b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsList.tsx index d77cc5e444..9abbfb69d9 100644 --- a/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsList.tsx +++ b/plugins/catalog/src/alpha/components/EntityTabs/EntityTabsList.tsx @@ -78,32 +78,104 @@ type TabGroup = { type EntityTabsListProps = { tabs: Tab[]; groupDefinitions: EntityContentGroupDefinitions; + defaultContentOrder?: 'title' | 'natural'; showIcons?: boolean; selectedIndex?: number; }; +function resolveGroupId( + tabGroup: string | undefined, + groupDefinitions: EntityContentGroupDefinitions, + aliasToGroup: Record, +): string | undefined { + if (!tabGroup) { + return undefined; + } + if (groupDefinitions[tabGroup]) { + return tabGroup; + } + return aliasToGroup[tabGroup]; +} + export function EntityTabsList(props: EntityTabsListProps) { const styles = useStyles(); const { t } = useTranslationRef(catalogTranslationRef); - const { tabs: items, selectedIndex = 0, showIcons, groupDefinitions } = props; + const { + tabs: items, + selectedIndex = 0, + showIcons, + groupDefinitions, + defaultContentOrder = 'title', + } = props; - const groups = useMemo( + const aliasToGroup = useMemo( () => - items.reduce((result, tab) => { - const group = tab.group ? groupDefinitions[tab.group] : undefined; - const groupOrId = group && tab.group ? tab.group : tab.id; - result[groupOrId] = result[groupOrId] ?? { - group, - items: [], - }; - result[groupOrId].items.push(tab); - return result; - }, {} as Record), - [items, groupDefinitions], + Object.entries(groupDefinitions).reduce((map, [groupId, def]) => { + for (const alias of def.aliases ?? []) { + map[alias] = groupId; + } + return map; + }, {} as Record), + [groupDefinitions], ); + const groups = useMemo(() => { + const byKey = items.reduce((result, tab) => { + const resolvedGroupId = resolveGroupId( + tab.group, + groupDefinitions, + aliasToGroup, + ); + const group = resolvedGroupId + ? groupDefinitions[resolvedGroupId] + : undefined; + const groupOrId = group && resolvedGroupId ? resolvedGroupId : tab.id; + result[groupOrId] = result[groupOrId] ?? { + group, + items: [], + }; + result[groupOrId].items.push(tab); + return result; + }, {} as Record); + + const groupOrder = Object.keys(groupDefinitions); + const sorted = Object.entries(byKey).sort(([a], [b]) => { + const ai = groupOrder.indexOf(a); + const bi = groupOrder.indexOf(b); + if (ai !== -1 && bi !== -1) { + return ai - bi; + } + if (ai !== -1) { + return -1; + } + if (bi !== -1) { + return 1; + } + return 0; + }); + + for (const [id, tabGroup] of sorted) { + const groupDef = groupDefinitions[id]; + if (groupDef) { + const order = groupDef.contentOrder ?? defaultContentOrder; + if (order === 'title') { + tabGroup.items.sort((a, b) => + a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }), + ); + } + } + } + + return sorted; + }, [items, groupDefinitions, aliasToGroup, defaultContentOrder]); + const selectedItem = items[selectedIndex]; + const selectedGroup = resolveGroupId( + selectedItem?.group, + groupDefinitions, + aliasToGroup, + ); return ( - {Object.entries(groups).map(([id, tabGroup]) => ( + {groups.map(([id, tabGroup]) => ( } /> - ); + return } />; }, }); }, @@ -80,9 +78,7 @@ export const catalogLinksEntityCard = EntityCardBlueprint.make({ type: 'info', filter: { 'metadata.links': { $exists: true } }, loader: async () => - import('../components/EntityLinksCard').then(m => ( - - )), + import('../components/EntityLinksCard').then(m => ), }, }); @@ -93,7 +89,7 @@ export const catalogLabelsEntityCard = EntityCardBlueprint.make({ filter: { 'metadata.labels': { $exists: true } }, loader: async () => import('../components/EntityLabelsCard').then(m => ( - + )), }, }); diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index e6bf4a05cf..a19c9017c0 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -428,6 +428,186 @@ describe('Entity page', () => { expect(screen.getAllByRole('tab')[1]).toHaveTextContent('Overview'); }); + it('Should resolve group aliases', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + { + config: { + groups: [ + { + docs: { title: 'Docs', aliases: ['documentation'] }, + }, + ], + }, + }, + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent); + + await renderInTestApp(tester.reactElement(), { + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }); + + await waitFor(() => + expect(screen.getByRole('tab', { name: /Docs/ })).toBeInTheDocument(), + ); + + await userEvent.click(screen.getByRole('tab', { name: /Docs/ })); + + await waitFor(() => + expect( + screen.getByRole('button', { name: /TechDocs/ }), + ).toHaveAttribute('href', '/techdocs'), + ); + + await waitFor(() => + expect(screen.getByRole('button', { name: /ApiDocs/ })).toHaveAttribute( + 'href', + '/apidocs', + ), + ); + }); + + it('Should sort content by title by default', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent); + + await renderInTestApp(tester.reactElement(), { + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }); + + await userEvent.click( + await screen.findByRole('tab', { name: /Documentation/ }), + ); + + const buttons = await screen.findAllByRole('button', { + name: /Docs/, + }); + expect(buttons[0]).toHaveTextContent('ApiDocs'); + expect(buttons[1]).toHaveTextContent('TechDocs'); + }); + + it('Should preserve natural order when configured', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + { + config: { + defaultContentOrder: 'natural', + }, + }, + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent); + + await renderInTestApp(tester.reactElement(), { + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }); + + await userEvent.click( + await screen.findByRole('tab', { name: /Documentation/ }), + ); + + const buttons = await screen.findAllByRole('button', { + name: /Docs/, + }); + expect(buttons[0]).toHaveTextContent('TechDocs'); + expect(buttons[1]).toHaveTextContent('ApiDocs'); + }); + + it('Should support per-group content order override', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + { + config: { + defaultContentOrder: 'title', + groups: [ + { + documentation: { + title: 'Documentation', + contentOrder: 'natural', + }, + }, + ], + }, + }, + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent); + + await renderInTestApp(tester.reactElement(), { + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }); + + await userEvent.click( + await screen.findByRole('tab', { name: /Documentation/ }), + ); + + const buttons = await screen.findAllByRole('button', { + name: /Docs/, + }); + expect(buttons[0]).toHaveTextContent('TechDocs'); + expect(buttons[1]).toHaveTextContent('ApiDocs'); + }); + it('Should render groups on the correct order', async () => { const tester = createExtensionTester( Object.assign({ namespace: 'catalog' }, catalogEntityPage), diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index aafbbd2178..6d64c65b74 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -31,6 +31,7 @@ import { EntityHeaderBlueprint, EntityContentGroupDefinitions, } from '@backstage/plugin-catalog-react/alpha'; +import CategoryIcon from '@material-ui/icons/Category'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; import { buildFilterFn } from './filter/FilterWrapper'; @@ -58,6 +59,8 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ return originalFactory({ path: '/catalog', routeRef: rootRouteRef, + icon: , + title: 'Catalog', loader: async () => { const { BaseCatalogPage } = await import('../components/CatalogPage'); const filters = inputs.filters.map(filter => @@ -106,16 +109,21 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ z.object({ title: z.string(), icon: z.string().optional(), + aliases: z.array(z.string()).optional(), + contentOrder: z.enum(['title', 'natural']).optional(), }), ), ) .optional(), + defaultContentOrder: z => + z.enum(['title', 'natural']).optional().default('title'), showNavItemIcons: z => z.boolean().optional().default(false), }, }, factory(originalFactory, { config, inputs }) { return originalFactory({ path: '/catalog/:namespace/:kind/:name', + title: 'Catalog Entity', // NOTE: The `convertLegacyRouteRef` call here ensures that this route ref // is mutated to support the new frontend system. Removing this conversion // is a potentially breaking change since this is a singleton and the @@ -170,6 +178,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ header={header} contextMenuItems={filteredMenuItems} groupDefinitions={groupDefinitions} + defaultContentOrder={config.defaultContentOrder} showNavItemIcons={config.showNavItemIcons} > {inputs.contents.map(output => ( diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 44cf2101b6..243b93db9b 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -15,8 +15,8 @@ */ import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import CategoryIcon from '@material-ui/icons/Category'; import { createComponentRouteRef, @@ -39,7 +39,11 @@ import contextMenuItems from './contextMenuItems'; /** @alpha */ export default createFrontendPlugin({ pluginId: 'catalog', - info: { packageJson: () => import('../../package.json') }, + title: 'Catalog', + icon: , + info: { + packageJson: () => import('../../package.json'), + }, routes: { catalogIndex: rootRouteRef, catalogEntity: entityRouteRef, diff --git a/plugins/catalog/src/alpha/translation.ts b/plugins/catalog/src/alpha/translation.ts index f346c408b9..3d48c0915a 100644 --- a/plugins/catalog/src/alpha/translation.ts +++ b/plugins/catalog/src/alpha/translation.ts @@ -111,6 +111,8 @@ export const catalogTranslationRef = createTranslationRef({ }, entityLabelsCard: { title: 'Labels', + columnKeyLabel: 'Label', + columnValueLabel: 'Value', emptyDescription: 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:', readMoreButtonTitle: 'Read more', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 8e90659520..065d786a13 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -16,11 +16,6 @@ import { useCallback } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; -import Divider from '@material-ui/core/Divider'; import IconButton from '@material-ui/core/IconButton'; import CachedIcon from '@material-ui/icons/Cached'; import EditIcon from '@material-ui/icons/Edit'; @@ -31,9 +26,9 @@ import { AppIcon, HeaderIconLinkRow, IconLinkVerticalProps, - InfoCardVariants, Link, } from '@backstage/core-components'; +import { EntityInfoCard } from '@backstage/plugin-catalog-react'; import { alertApiRef, errorApiRef, @@ -77,6 +72,16 @@ import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { catalogTranslationRef } from '../../alpha/translation'; import { useSourceTemplateCompoundEntityRef } from './hooks'; import { AboutContent } from './AboutContent'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + linkContainer: { + border: '1px solid var(--bui-border-1)', + borderLeft: 'none', + borderRight: 'none', + marginBottom: 'var(--bui-space-6)', + }, +}); export function useCatalogSourceIconLinkProps() { const { entity } = useEntity(); @@ -152,41 +157,13 @@ function DefaultAboutCardSubheader() { return ; } -const useStyles = makeStyles({ - gridItemCard: { - display: 'flex', - flexDirection: 'column', - height: 'calc(100% - 10px)', // for pages without content header - marginBottom: '10px', - }, - fullHeightCard: { - display: 'flex', - flexDirection: 'column', - height: '100%', - }, - gridItemCardContent: { - flex: 1, - }, - fullHeightCardContent: { - flex: 1, - }, -}); - -/** - * Props for {@link EntityAboutCard}. - * - * @public - */ -export type AboutCardProps = { - variant?: InfoCardVariants; -}; - -export interface InternalAboutCardProps extends AboutCardProps { - subheader?: JSX.Element; +export interface InternalAboutCardProps { + /** Icon link row rendered at the top of the card body. */ + iconLinks?: JSX.Element; } export function InternalAboutCard(props: InternalAboutCardProps) { - const { variant, subheader } = props; + const { iconLinks } = props; const classes = useStyles(); const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); @@ -202,20 +179,6 @@ export function InternalAboutCard(props: InternalAboutCardProps) { const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - let cardClass = ''; - if (variant === 'gridItem') { - cardClass = classes.gridItemCard; - } else if (variant === 'fullHeight') { - cardClass = classes.fullHeightCard; - } - - let cardContentClass = ''; - if (variant === 'gridItem') { - cardContentClass = classes.gridItemCardContent; - } else if (variant === 'fullHeight') { - cardContentClass = classes.fullHeightCardContent; - } - const entityLocation = entity.metadata.annotations?.[ANNOTATION_LOCATION]; // Limiting the ability to manually refresh to the less expensive locations const allowRefresh = @@ -234,60 +197,58 @@ export function InternalAboutCard(props: InternalAboutCardProps) { }, [catalogApi, entity, alertApi, t, errorApi]); return ( - - - {allowRefresh && canRefresh && ( - - - - )} + + {allowRefresh && canRefresh && ( + + + + )} + + + + {sourceTemplateRef && templateRoute && ( - + - {sourceTemplateRef && templateRoute && ( - - - - )} - - } - subheader={subheader ?? } - /> - - - - - + )} + + } + > +
    + {iconLinks ?? } +
    + +
    ); } /** * Exported publicly via the EntityAboutCard * - * NOTE: We generally do not accept pull requests to extend this class with more - * props and customizability. If you need to tweak it, consider making a bespoke - * card in your own repository instead, that is perfect for your own needs. + * NOTE: We generally do not accept pull requests to extend this class with props + * and customizability. If you need to tweak it, consider making a bespoke card + * in your own repository instead, that is perfect for your own needs. */ -export function AboutCard(props: AboutCardProps) { - return ; +export function AboutCard() { + return ; } diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index d23a29a7c5..eff9ae7da3 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -26,8 +26,8 @@ import { } from '@backstage/plugin-catalog-react'; import { JsonArray } from '@backstage/types'; import Chip from '@material-ui/core/Chip'; -import Grid from '@material-ui/core/Grid'; import { makeStyles } from '@material-ui/core/styles'; +import { Grid } from '@backstage/ui'; import { MarkdownContent } from '@backstage/core-components'; import { AboutField } from './AboutField'; import { LinksGridList } from '../EntityLinksCard/LinksGridList'; @@ -114,25 +114,25 @@ export function AboutContent(props: AboutContentProps) { entitySourceLocation = undefined; } + const columns = { initial: '1', sm: '2', lg: '3' } as const; + return ( - - - - + + + + + + {ownedByRelations.length > 0 && ( @@ -142,7 +142,6 @@ export function AboutContent(props: AboutContentProps) { {partOfDomainRelations.length > 0 && ( {partOfSystemRelations.length > 0 && ( )} {(isAPI || @@ -200,38 +196,37 @@ export function AboutContent(props: AboutContentProps) { )} {(entity?.metadata?.tags || []).map(tag => ( ))} {isLocation && (entity?.spec?.targets || entity?.spec?.target) && ( - - target as string) - .map(target => ({ - text: target, - href: getLocationTargetHref( - target, - (entity?.spec?.type || t('aboutCard.unknown')) as string, - entitySourceLocation!, - ), - }))} - /> - + + + target as string) + .map(target => ({ + text: target, + href: getLocationTargetHref( + target, + (entity?.spec?.type || t('aboutCard.unknown')) as string, + entitySourceLocation!, + ), + }))} + /> + + )} - + ); } diff --git a/plugins/catalog/src/components/AboutCard/AboutField.tsx b/plugins/catalog/src/components/AboutCard/AboutField.tsx index 7ced57e542..725a82612a 100644 --- a/plugins/catalog/src/components/AboutCard/AboutField.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutField.tsx @@ -15,7 +15,6 @@ */ import { useElementFilter } from '@backstage/core-plugin-api'; -import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { ReactNode } from 'react'; @@ -48,14 +47,13 @@ const useStyles = makeStyles(theme => ({ export interface AboutFieldProps { label: string; value?: string; - gridSizes?: Record; children?: ReactNode; className?: string; } /** @public */ export function AboutField(props: AboutFieldProps) { - const { label, value, gridSizes, children, className } = props; + const { label, value, children, className } = props; const classes = useStyles(); const { t } = useTranslationRef(catalogTranslationRef); @@ -71,11 +69,11 @@ export function AboutField(props: AboutFieldProps) { ); return ( - +
    {label} {content} - +
    ); } diff --git a/plugins/catalog/src/components/AboutCard/index.ts b/plugins/catalog/src/components/AboutCard/index.ts index 9660a5e2e2..f6a11dfbb7 100644 --- a/plugins/catalog/src/components/AboutCard/index.ts +++ b/plugins/catalog/src/components/AboutCard/index.ts @@ -15,7 +15,6 @@ */ export { AboutCard } from './AboutCard'; -export type { AboutCardProps } from './AboutCard'; export { AboutContent } from './AboutContent'; export type { AboutContentProps } from './AboutContent'; export { AboutField } from './AboutField'; diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index 04c182b5be..95985f31c3 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -14,77 +14,77 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-react'; -import { - InfoCard, - InfoCardVariants, - Table, - TableColumn, -} from '@backstage/core-components'; +import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react'; import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { + Table, + CellText, + useTable, + type ColumnConfig, + type TableItem, +} from '@backstage/ui'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { useMemo } from 'react'; /** @public */ export interface EntityLabelsCardProps { - variant?: InfoCardVariants; title?: string; } -const useStyles = makeStyles(_ => ({ - key: { - fontWeight: 'bold', - }, -})); +interface LabelItem extends TableItem { + id: string; + key: string; + value: string; +} export const EntityLabelsCard = (props: EntityLabelsCardProps) => { - const { variant, title } = props; + const { title } = props; const { entity } = useEntity(); - const classes = useStyles(); const { t } = useTranslationRef(catalogTranslationRef); - const columns: TableColumn<{ key: string; value: string }>[] = [ - { - render: row => { - return ( - - {row.key} - - ); - }, - }, - { - field: 'value', - }, - ]; - const labels = entity?.metadata?.labels; + const columnConfig: ColumnConfig[] = useMemo( + () => [ + { + id: 'key', + label: t('entityLabelsCard.columnKeyLabel'), + isRowHeader: true, + cell: item => , + }, + { + id: 'value', + label: t('entityLabelsCard.columnValueLabel'), + cell: item => , + }, + ], + [t], + ); + + const data = useMemo( + () => + Object.keys(labels ?? {}).map(labelKey => ({ + id: labelKey, + key: labelKey, + value: labels![labelKey], + })), + [labels], + ); + + const { tableProps } = useTable({ + mode: 'complete', + data, + paginationOptions: { pageSize: 5 }, + }); + return ( - + {!labels || Object.keys(labels).length === 0 ? ( ) : ( -
    ({ - key: labelKey, - value: labels[labelKey], - }))} - options={{ - search: false, - showTitle: true, - loadingType: 'linear', - header: false, - padding: 'dense', - pageSize: 5, - toolbar: false, - paging: Object.keys(labels).length > 5, - }} - /> +
    )} - + ); }; diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index 39088b0a16..e090effb55 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -14,24 +14,22 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-react'; +import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react'; import LanguageIcon from '@material-ui/icons/Language'; import { EntityLinksEmptyState } from './EntityLinksEmptyState'; import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; -import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { catalogTranslationRef } from '../../alpha/translation'; /** @public */ export interface EntityLinksCardProps { cols?: ColumnBreakpoints | number; - variant?: InfoCardVariants; } export const EntityLinksCard = (props: EntityLinksCardProps) => { - const { cols = undefined, variant } = props; + const { cols = undefined } = props; const { entity } = useEntity(); const app = useApp(); const { t } = useTranslationRef(catalogTranslationRef); @@ -42,7 +40,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const links = entity?.metadata?.links; return ( - + {!links || links.length === 0 ? ( ) : ( @@ -55,6 +53,6 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { }))} /> )} - + ); }; diff --git a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx index 797428df33..dfb0b280ff 100644 --- a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ -import ImageList from '@material-ui/core/ImageList'; -import ImageListItem from '@material-ui/core/ImageListItem'; +import { Grid, type Columns } from '@backstage/ui'; import { IconLink } from './IconLink'; import { ColumnBreakpoints } from './types'; import { useDynamicColumns } from './useDynamicColumns'; @@ -37,12 +36,13 @@ export function LinksGridList(props: LinksGridListProps) { const numOfCols = useDynamicColumns(cols); return ( - + {items.map(({ text, href, Icon }, i) => ( - - - + ))} - + ); } diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index f3cc7d3d0b..a59283c677 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -23,7 +23,6 @@ export * from './apis'; export type { - AboutCardProps, AboutContentProps, AboutFieldProps, } from './components/AboutCard'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 50d858a45f..6fe531418b 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -43,7 +43,6 @@ import { SearchResultListItemExtensionProps, } from '@backstage/plugin-search-react'; import { DefaultStarredEntitiesApi } from './apis'; -import { AboutCardProps } from './components/AboutCard'; import { DefaultCatalogPageProps } from './components/CatalogPage'; import { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; import { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard'; @@ -128,15 +127,14 @@ export const CatalogEntityPage: () => JSX.Element = catalogPlugin.provide( * not extremely customizable; feel free to make a copy of it as a starting * point if you like. */ -export const EntityAboutCard: (props: AboutCardProps) => JSX.Element = - catalogPlugin.provide( - createComponentExtension({ - name: 'EntityAboutCard', - component: { - lazy: () => import('./components/AboutCard').then(m => m.AboutCard), - }, - }), - ); +export const EntityAboutCard: () => JSX.Element = catalogPlugin.provide( + createComponentExtension({ + name: 'EntityAboutCard', + component: { + lazy: () => import('./components/AboutCard').then(m => m.AboutCard), + }, + }), +); /** @public */ export const EntityLinksCard = catalogPlugin.provide( diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index c8e1c99fb8..b617811885 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-config-schema +## 0.1.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.77 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.77-next.1 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index d603cbaf4e..c643d6b1f4 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.77-next.1", + "version": "0.1.78-next.0", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 4223746f96..4edca5067a 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-devtools-backend +## 0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/cli-common@0.2.0-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.5.14 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/config-loader@1.10.8 + - @backstage/cli-common@0.1.18 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-devtools-common@0.1.22 + ## 0.5.14-next.1 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index cf5c0230f5..58fe503a18 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.5.14-next.1", + "version": "0.5.15-next.1", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 3385d9e3ba..69ce927d59 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-devtools-common +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + ## 0.1.22-next.0 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index 17c5f1e60e..6c7fec212a 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.22-next.0", + "version": "0.1.22", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library", diff --git a/plugins/devtools-common/src/alpha.ts b/plugins/devtools-common/src/alpha.ts index 984967c4f3..7abe4b1f5e 100644 --- a/plugins/devtools-common/src/alpha.ts +++ b/plugins/devtools-common/src/alpha.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { devToolsTaskSchedulerReadPermission, devToolsTaskSchedulerCreatePermission, diff --git a/plugins/devtools-react/CHANGELOG.md b/plugins/devtools-react/CHANGELOG.md index 4ecb6a8416..83e7ebf7fd 100644 --- a/plugins/devtools-react/CHANGELOG.md +++ b/plugins/devtools-react/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-devtools-react +## 0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.1.1 + +### Patch Changes + +- 9fbb270: Updated dependency `@testing-library/react` to `^16.0.0`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.1-next.1 ### Patch Changes diff --git a/plugins/devtools-react/package.json b/plugins/devtools-react/package.json index 6d6de7f9eb..4f762633b3 100644 --- a/plugins/devtools-react/package.json +++ b/plugins/devtools-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-react", - "version": "0.1.1-next.1", + "version": "0.1.2-next.1", "description": "Web library for the devtools plugin", "backstage": { "role": "web-library", diff --git a/plugins/devtools-react/report.api.md b/plugins/devtools-react/report.api.md index d1b1a42250..6916afed84 100644 --- a/plugins/devtools-react/report.api.md +++ b/plugins/devtools-react/report.api.md @@ -15,7 +15,6 @@ export const DevToolsContentBlueprint: ExtensionBlueprint<{ params: DevToolsContentBlueprintParams; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', @@ -23,6 +22,7 @@ export const DevToolsContentBlueprint: ExtensionBlueprint<{ optional: true; } > + | ExtensionDataRef | ExtensionDataRef; inputs: {}; config: { diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index a33e7292cb..c9b87d0f52 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-devtools +## 0.1.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-devtools-react@0.1.2-next.1 + +## 0.1.37-next.1 + +### Patch Changes + +- afabb37: Fixed URL encoding of task IDs for the trigger feature (tasks that contained a "/" in their ID were not triggered) +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.1.36 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- f2612c2: Fixes an issue where a user lacking permission to schedule tasks can now easily see the issue through a custom icon + tooltip. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-devtools-react@0.1.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-devtools-common@0.1.22 + ## 0.1.36-next.1 ### Patch Changes diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index b23c35d4dd..3f9a3da747 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -66,42 +66,14 @@ You need to setup the [DevTools backend plugin](../devtools-backend/README.md) b ### Frontend -To setup the DevTools frontend you'll need to do the following steps: +Install the `@backstage/plugin-devtools` package in your frontend app: -1. First we need to add the `@backstage/plugin-devtools` package to your frontend app: +```sh +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-devtools +``` - ```sh - # From your Backstage root directory - yarn --cwd packages/app add @backstage/plugin-devtools - ``` - -2. Now open the `packages/app/src/App.tsx` file -3. Then after all the import statements add the following line: - - ```ts - import { DevToolsPage } from '@backstage/plugin-devtools'; - ``` - -4. In this same file just before the closing ``, this will be near the bottom of the file, add this line: - - ```ts - } /> - ``` - -5. Next open the `packages/app/src/components/Root/Root.tsx` file -6. We want to add this icon import after all the existing import statements: - - ```ts - import BuildIcon from '@material-ui/icons/Build'; - ``` - -7. Then add this line just after the `` line: - - ```ts - - ``` - -8. Now run `yarn start` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info) +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). ## Customizing @@ -166,8 +138,6 @@ You can also add tabs to show content from other plugins that fit well with the #### Catalog Unprocessed Entities Tab -##### New Frontend System - Create an extension and/or load a 3rd party extension to add additional tabs. ```shell @@ -197,7 +167,7 @@ const appFeature = createFrontendModule({ }); ``` -##### Old System +##### Old Frontend System Here's how to add the Catalog Unprocessed Entities tab: @@ -230,6 +200,38 @@ Here's how to add the Catalog Unprocessed Entities tab: 4. Now run `yarn start` and navigate to the DevTools you'll see a new tab for Unprocessed Entities +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + +1. Open the `packages/app/src/App.tsx` file +2. Then after all the import statements add the following line: + + ```ts + import { DevToolsPage } from '@backstage/plugin-devtools'; + ``` + +3. In this same file just before the closing ``, this will be near the bottom of the file, add this line: + + ```ts + } /> + ``` + +4. Next open the `packages/app/src/components/Root/Root.tsx` file +5. We want to add this icon import after all the existing import statements: + + ```ts + import BuildIcon from '@material-ui/icons/Build'; + ``` + +6. Then add this line just after the `` line: + + ```ts + + ``` + +7. Now run `yarn start` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info) + ## Permissions The DevTools plugin supports the [permissions framework](https://backstage.io/docs/permissions/overview), the following sections outline how you can use them with the assumption that you have the permissions framework setup and working. diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 0322cd737e..36ff2b8452 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.36-next.1", + "version": "0.1.37-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools/report-alpha.api.md b/plugins/devtools/report-alpha.api.md index 8f3e24f7f3..753875eaaa 100644 --- a/plugins/devtools/report-alpha.api.md +++ b/plugins/devtools/report-alpha.api.md @@ -11,6 +11,7 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -63,21 +64,67 @@ const _default: OverridableFrontendPlugin< 'page:devtools': OverridableExtensionDefinition<{ config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; contents: ExtensionInput< | ConfigurableExtensionDataRef | ConfigurableExtensionDataRef @@ -99,10 +146,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/devtools/report.api.md b/plugins/devtools/report.api.md index 804e678e89..a74dd7eb23 100644 --- a/plugins/devtools/report.api.md +++ b/plugins/devtools/report.api.md @@ -17,7 +17,7 @@ export const ConfigContent: () => JSX_2.Element; // @public export const DevToolsLayout: { - ({ children, title, subtitle }: DevToolsLayoutProps): JSX_2.Element; + (input: DevToolsLayoutProps): JSX_2.Element; Route: (props: SubRoute) => null; }; @@ -29,7 +29,7 @@ export type DevToolsLayoutProps = { }; // @public (undocumented) -export const DevToolsPage: ({ contents }: DevToolsPageProps) => JSX_2.Element; +export const DevToolsPage: (input: DevToolsPageProps) => JSX_2.Element; // @public (undocumented) export interface DevToolsPageContent { @@ -62,9 +62,7 @@ export const ExternalDependenciesContent: () => JSX_2.Element; export const InfoContent: () => JSX_2.Element; // @public (undocumented) -export const ScheduledTaskDetailPanel: ({ - rowData, -}: { +export const ScheduledTaskDetailPanel: (input: { rowData: TaskApiTasksResponse; }) => JSX_2.Element; diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index edeaae5723..b991e34bf9 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -88,6 +88,8 @@ export const devToolsNavItem = NavItemBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'devtools', + title: 'DevTools', + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts index 4fc579ee06..2aaad49aa8 100644 --- a/plugins/devtools/src/api/DevToolsApi.ts +++ b/plugins/devtools/src/api/DevToolsApi.ts @@ -38,4 +38,5 @@ export interface DevToolsApi { plugin: string, taskId: string, ): Promise; + cancelScheduledTask(plugin: string, taskId: string): Promise; } diff --git a/plugins/devtools/src/api/DevToolsClient.test.ts b/plugins/devtools/src/api/DevToolsClient.test.ts new file mode 100644 index 0000000000..d1972cd528 --- /dev/null +++ b/plugins/devtools/src/api/DevToolsClient.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2026 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 { DevToolsClient } from './DevToolsClient'; + +describe('DevToolsClient', () => { + const mockBaseUrl = 'http://backstage/api/catalog'; + const discoveryApi = { + getBaseUrl: async (pluginId: string) => `${mockBaseUrl}/${pluginId}`, + }; + const mockFetch = jest.fn(); + const fetchApi = { fetch: mockFetch }; + + let client: DevToolsClient; + beforeEach(() => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ status: 'triggered' }), + }); + client = new DevToolsClient({ discoveryApi, fetchApi }); + }); + + afterEach(() => jest.resetAllMocks()); + + it('should URL-encode the taskId when triggering a scheduled task', async () => { + await client.triggerScheduledTask( + 'my-plugin', + 'task/with/slashes:and-special-chars', + ); + + expect(mockFetch).toHaveBeenCalledWith( + `${mockBaseUrl}/my-plugin/.backstage/scheduler/v1/tasks/task%2Fwith%2Fslashes%3Aand-special-chars/trigger`, + { method: 'POST' }, + ); + }); +}); diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts index cd4f53fa81..b6d9d4b119 100644 --- a/plugins/devtools/src/api/DevToolsClient.ts +++ b/plugins/devtools/src/api/DevToolsClient.ts @@ -24,7 +24,7 @@ import { ScheduledTasks, TriggerScheduledTask, } from '@backstage/plugin-devtools-common/alpha'; -import { ResponseError } from '@backstage/errors'; +import { ResponseError, NotFoundError, ConflictError } from '@backstage/errors'; import { DevToolsApi } from './DevToolsApi'; export class DevToolsClient implements DevToolsApi { @@ -70,7 +70,7 @@ export class DevToolsClient implements DevToolsApi { ): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`; const url = new URL( - `.backstage/scheduler/v1/tasks/${taskId}/trigger`, + `.backstage/scheduler/v1/tasks/${encodeURIComponent(taskId)}/trigger`, baseUrl, ); @@ -82,7 +82,31 @@ export class DevToolsClient implements DevToolsApi { throw await ResponseError.fromResponse(response); } - return response.json() as Promise; + return {}; + } + + public async cancelScheduledTask( + plugin: string, + taskId: string, + ): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`; + const url = new URL( + `.backstage/scheduler/v1/tasks/${encodeURIComponent(taskId)}/cancel`, + baseUrl, + ); + + const response = await this.fetchApi.fetch(url.toString(), { + method: 'POST', + }); + + if (!response.ok) { + if (response.status === 404) { + throw new NotFoundError(`Task ${taskId} not found`); + } else if (response.status === 409) { + throw new ConflictError(`Task ${taskId} is not running`); + } + throw await ResponseError.fromResponse(response); + } } public async getExternalDependencies(): Promise< diff --git a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx index 7517cf8ea5..af86b9b237 100644 --- a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx +++ b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx @@ -29,10 +29,11 @@ import { TableColumn, } from '@backstage/core-components'; import Alert from '@material-ui/lab/Alert'; -import { useScheduledTasks, useTriggerScheduledTask } from '../../../hooks'; +import { useScheduledTasks, useScheduledTasksOperations } from '../../../hooks'; import { TaskApiTasksResponse } from '@backstage/plugin-devtools-common/alpha'; import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api'; import RefreshIcon from '@material-ui/icons/Refresh'; +import StopIcon from '@material-ui/icons/Stop'; import NightsStay from '@material-ui/icons/NightsStay'; import ErrorIcon from '@material-ui/icons/Error'; import BlockIcon from '@material-ui/icons/Block'; @@ -105,7 +106,7 @@ export const ScheduledTasksContent = () => { configApi.getOptionalStringArray('devTools.scheduledTasks.plugins') || []; const [selectedPlugin, setSelectedPlugin] = useState(plugins[0] || ''); const { scheduledTasks, loading, error } = useScheduledTasks(selectedPlugin); - const { triggerTask, isTriggering, triggerError } = useTriggerScheduledTask(); + const { triggerTask, cancelTask, isLoading } = useScheduledTasksOperations(); const [inputValue, setInputValue] = useState(''); @@ -209,28 +210,52 @@ export const ScheduledTasksContent = () => { permission={devToolsTaskSchedulerCreatePermission} errorPage={} > - - { - triggerTask(selectedPlugin, rowData.taskId); - if (triggerError) { - alertApi.post({ - message: `Error triggering task ${rowData.taskId}: ${error}`, - severity: 'error', - }); - } else { - alertApi.post({ - message: `Successfully triggered task ${rowData.taskId}`, - severity: 'success', - }); - } - }} - > - - - + + + { + try { + await triggerTask(selectedPlugin, rowData.taskId); + alertApi.post({ + message: `Successfully triggered task ${rowData.taskId}`, + severity: 'success', + }); + } catch (e) { + alertApi.post({ + message: `Error triggering task ${rowData.taskId}: ${e.message}`, + severity: 'error', + }); + } + }} + > + + + + + { + try { + await cancelTask(selectedPlugin, rowData.taskId); + alertApi.post({ + message: `Successfully cancelled task ${rowData.taskId}`, + severity: 'success', + }); + } catch (e) { + alertApi.post({ + message: `Error cancelling task ${rowData.taskId}: ${e.message}`, + severity: 'error', + }); + } + }} + > + + + + ), sorting: false, @@ -263,7 +288,7 @@ export const ScheduledTasksContent = () => { )} /> - {loading && } + {loading && !scheduledTasks && } {error && ( { )} - {!loading && !error && ( + {scheduledTasks && (
    { search: true, sorting: true, searchFieldAlignment: 'right', + padding: 'dense', }} columns={columns} data={scheduledTasks || []} diff --git a/plugins/devtools/src/hooks/index.ts b/plugins/devtools/src/hooks/index.ts index 105d03fb68..9d212bc3af 100644 --- a/plugins/devtools/src/hooks/index.ts +++ b/plugins/devtools/src/hooks/index.ts @@ -18,4 +18,4 @@ export { useConfig } from './useConfig'; export { useExternalDependencies } from './useExternalDependencies'; export { useInfo } from './useInfo'; export { useScheduledTasks } from './useScheduledTasks'; -export { useTriggerScheduledTask } from './useTriggerScheduledTask'; +export { useScheduledTasksOperations } from './useScheduledTasksOperations'; diff --git a/plugins/devtools/src/hooks/useTriggerScheduledTask.ts b/plugins/devtools/src/hooks/useScheduledTasksOperations.ts similarity index 66% rename from plugins/devtools/src/hooks/useTriggerScheduledTask.ts rename to plugins/devtools/src/hooks/useScheduledTasksOperations.ts index d26b7a8025..ff9d02b675 100644 --- a/plugins/devtools/src/hooks/useTriggerScheduledTask.ts +++ b/plugins/devtools/src/hooks/useScheduledTasksOperations.ts @@ -17,22 +17,40 @@ import { useState, useCallback } from 'react'; import { devToolsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; -export const useTriggerScheduledTask = () => { +export const useScheduledTasksOperations = () => { const api = useApi(devToolsApiRef); - const [isTriggering, setIsTriggering] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); const triggerTask = useCallback( async (plugin: string, taskId: string) => { - setIsTriggering(true); + setIsLoading(true); setError(undefined); try { await api.triggerScheduledTask(plugin, taskId); } catch (e) { setError(e); + throw e; } finally { - setIsTriggering(false); + setIsLoading(false); + } + }, + [api], + ); + + const cancelTask = useCallback( + async (plugin: string, taskId: string) => { + setIsLoading(true); + setError(undefined); + + try { + await api.cancelScheduledTask(plugin, taskId); + } catch (e) { + setError(e); + throw e; + } finally { + setIsLoading(false); } }, [api], @@ -40,7 +58,8 @@ export const useTriggerScheduledTask = () => { return { triggerTask, - isTriggering, - triggerError: error?.message, + cancelTask, + isLoading, + error: error?.message, }; }; diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 43ad9d67e0..6705f5c3a8 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.4.19-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 6070fa3bb1..8ababc2055 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.4.19-next.0", + "version": "0.4.20-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index dc6cabb5b5..009b781920 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.2.28-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index c0727a0d97..0bcca04edc 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.28-next.0", + "version": "0.2.29-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 1a48ee55eb..f7b29791b4 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.2.28-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index d855b0bf0b..917c8a9792 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.28-next.0", + "version": "0.2.29-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md index b89c8aa025..8e0176b51b 100644 --- a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-events-backend-module-bitbucket-server +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 98873d5f35..47baa5ef78 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 80abacb894..f46096fe7b 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.2.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.2.28-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 5ed9057ea1..e8cbf32a1b 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.28-next.0", + "version": "0.2.29-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index d4b2ed0997..55c93f9f40 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-events-backend-module-github +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.4.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index c969d661c3..630a47da5b 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.4.9-next.1", + "version": "0.4.10-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index a28a72c41c..118ae100a9 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.3.9-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index c25b0e87ef..7cfb21c86f 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.3.9-next.0", + "version": "0.3.10-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-google-pubsub/CHANGELOG.md b/plugins/events-backend-module-google-pubsub/CHANGELOG.md index 7ec8628fa5..e8da334960 100644 --- a/plugins/events-backend-module-google-pubsub/CHANGELOG.md +++ b/plugins/events-backend-module-google-pubsub/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-events-backend-module-google-pubsub +## 0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.2.0 + +### Minor Changes + +- 80905b3: Added an optional `filter` property to PubSub consumers/publishers + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/filter-predicates@0.1.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.1.8-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-google-pubsub/package.json b/plugins/events-backend-module-google-pubsub/package.json index 27bf5413ef..9c5eedc53e 100644 --- a/plugins/events-backend-module-google-pubsub/package.json +++ b/plugins/events-backend-module-google-pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-google-pubsub", - "version": "0.1.8-next.0", + "version": "0.2.1-next.1", "description": "The google-pubsub backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-module-kafka/CHANGELOG.md b/plugins/events-backend-module-kafka/CHANGELOG.md index 4149df548b..beaa679787 100644 --- a/plugins/events-backend-module-kafka/CHANGELOG.md +++ b/plugins/events-backend-module-kafka/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-events-backend-module-kafka +## 0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.3.1-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-kafka/package.json b/plugins/events-backend-module-kafka/package.json index 3f3f3e7e6d..891edd4a2b 100644 --- a/plugins/events-backend-module-kafka/package.json +++ b/plugins/events-backend-module-kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-kafka", - "version": "0.3.1-next.0", + "version": "0.3.2-next.1", "description": "The kafka backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 4cdbe56043..4e48326968 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.1.52 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.19 + ## 0.1.52-next.0 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index a8bc69ae17..e5ea9a9e90 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.52-next.0", + "version": "0.1.53-next.0", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 6e20caa7e1..da085c1807 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-events-backend +## 0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.6.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.5.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index 583ce51afd..40581ebe01 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.5.11-next.0", + "version": "0.6.0-next.2", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-backend/src/schema/openapi.yaml b/plugins/events-backend/src/schema/openapi.yaml index 97f7705f87..6ff064d660 100644 --- a/plugins/events-backend/src/schema/openapi.yaml +++ b/plugins/events-backend/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: events version: '1' @@ -17,7 +17,6 @@ components: name: subscriptionId in: path required: true - allowReserved: true schema: type: string requestBodies: {} diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts index 0b116fe012..62130143f9 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts index 8d81cbaf39..9a0ffed740 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/index.ts b/plugins/events-backend/src/schema/openapi/generated/index.ts index dec4b8804e..58fc52487a 100644 --- a/plugins/events-backend/src/schema/openapi/generated/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 809cd3fa80..d72822b815 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts index 02e52a81d9..04cf4b5d3d 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,8 +26,5 @@ export interface Event { * The topic that the event is published on */ topic: string; - /** - * The event payload - */ payload: any | null; } diff --git a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index ff15eef3ce..cd0b45e19d 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3914d73531..c4af31b48c 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts index 6bbb2974b5..c097e10d11 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index 786f0a0644..dabc823807 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/index.ts b/plugins/events-backend/src/schema/openapi/generated/models/index.ts index 6baa8ef449..b3cd7dc096 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-backend/src/schema/openapi/generated/router.ts b/plugins/events-backend/src/schema/openapi/generated/router.ts index f1b9de2099..ce89146f9b 100644 --- a/plugins/events-backend/src/schema/openapi/generated/router.ts +++ b/plugins/events-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: 'events', version: '1', @@ -45,7 +45,6 @@ export const spec = { name: 'subscriptionId', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, diff --git a/plugins/events-backend/src/schema/openapi/index.ts b/plugins/events-backend/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/plugins/events-backend/src/schema/openapi/index.ts +++ b/plugins/events-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index b2f2f4dde3..d1211a2779 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-events-node +## 0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 0.4.19-next.0 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 1696d67324..96663c059a 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.4.19-next.0", + "version": "0.4.20-next.1", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts index 71e3767af2..a03ea2e7fd 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,6 +17,7 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** + import { DiscoveryApi } from '../types/discovery'; import { FetchApi } from '../types/fetch'; import crossFetch from 'cross-fetch'; diff --git a/plugins/events-node/src/schema/openapi/generated/apis/index.ts b/plugins/events-node/src/schema/openapi/generated/apis/index.ts index fc7c83b736..c2e931caf8 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/index.ts b/plugins/events-node/src/schema/openapi/generated/index.ts index dc3055033d..19501a5dcb 100644 --- a/plugins/events-node/src/schema/openapi/generated/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts index 809cd3fa80..d72822b815 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts index 02e52a81d9..04cf4b5d3d 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,8 +26,5 @@ export interface Event { * The topic that the event is published on */ topic: string; - /** - * The event payload - */ payload: any | null; } diff --git a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index ff15eef3ce..cd0b45e19d 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts index 3914d73531..c4af31b48c 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts index 6bbb2974b5..c097e10d11 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index 786f0a0644..dabc823807 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/models/index.ts b/plugins/events-node/src/schema/openapi/generated/models/index.ts index 6baa8ef449..b3cd7dc096 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/generated/pluginId.ts b/plugins/events-node/src/schema/openapi/generated/pluginId.ts index b5fc50cfc0..5351088e31 100644 --- a/plugins/events-node/src/schema/openapi/generated/pluginId.ts +++ b/plugins/events-node/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/events-node/src/schema/openapi/index.ts b/plugins/events-node/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/plugins/events-node/src/schema/openapi/index.ts +++ b/plugins/events-node/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index 5313c7a0f9..e503b5ea33 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @internal/plugin-todo-list-backend +## 1.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 1.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + +## 1.0.47 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 1.0.47-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 03fb2b3ac4..bf0ea3aaeb 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.47-next.0", + "version": "1.0.48-next.1", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index 87af2d6553..c66f74ef75 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + ## 1.0.29-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 2b33fada42..5fefb00256 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.29-next.0", + "version": "1.0.29", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 9214555ece..7a17541264 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,21 @@ # @internal/plugin-todo-list +## 1.0.49-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 1.0.48 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-plugin-api@1.12.3 + ## 1.0.48-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 5db2097554..d6065a1f9c 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.48-next.0", + "version": "1.0.49-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/gateway-backend/CHANGELOG.md b/plugins/gateway-backend/CHANGELOG.md index d6fd09fff8..f8b8ccad9d 100644 --- a/plugins/gateway-backend/CHANGELOG.md +++ b/plugins/gateway-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-gateway-backend +## 1.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## 1.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 1.1.2-next.0 ### Patch Changes diff --git a/plugins/gateway-backend/package.json b/plugins/gateway-backend/package.json index 342767da9e..e587a17345 100644 --- a/plugins/gateway-backend/package.json +++ b/plugins/gateway-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gateway-backend", - "version": "1.1.2-next.0", + "version": "1.1.3-next.1", "backstage": { "role": "backend-plugin", "pluginId": "gateway", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 9aaa30bfd1..2d0637a181 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-home-react +## 0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.1.35 + +### Patch Changes + +- 90956a6: Support new frontend system in the homepage plugin +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/core-compat-api@0.5.8 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.35-next.1 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index 2fd0df87c1..f75c6c2bd3 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.35-next.1", + "version": "0.1.36-next.1", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index ba26964481..81af7fe79b 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-home +## 0.9.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.2 + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-home-react@0.1.36-next.1 + +## 0.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## 0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## 0.9.2 + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 90956a6: Support new frontend system in the homepage plugin +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/core-compat-api@0.5.8 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-home-react@0.1.35 + ## 0.9.2-next.2 ### Patch Changes diff --git a/plugins/home/README.md b/plugins/home/README.md index 04d1b8c2ec..2c46530c3d 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -6,40 +6,19 @@ For App Integrators, the system is designed to be composable to give total freed ## Installation -If you have a standalone app (you didn't clone this repo), then do - ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-home ``` -## Getting started - -The home plugin supports both the new frontend system and the legacy system. - -### New Frontend System - -If you're using Backstage's new frontend system, add the plugin to your app: - -```ts -// packages/app/src/App.tsx -import homePlugin from '@backstage/plugin-home/alpha'; - -const app = createApp({ - features: [ - // ... other plugins - homePlugin, - // ... other plugins - ], -}); -``` +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). The plugin will automatically provide: - A homepage at `/home` with customizable widget grid - A "Home" navigation item in the sidebar -#### Creating Custom Homepage Layouts +## Creating Custom Homepage Layouts Use the `HomePageLayoutBlueprint` from `@backstage/plugin-home-react/alpha` to create custom homepage layouts. A layout receives the installed widgets and is @@ -86,7 +65,7 @@ const homeModule = createFrontendModule({ }); ``` -#### Visit Tracking (Optional) +## Visit Tracking (Optional) Visit tracking is an **optional feature** that must be explicitly enabled. When enabled, it provides intelligent storage fallbacks: @@ -112,12 +91,7 @@ app: ## Creating Homepage Widgets -Homepage widgets are React components that can be added to customizable home pages. The **key difference** between the new frontend system and legacy system is how these widget components are **registered and exported**: - -- **New Frontend System**: Use `HomePageWidgetBlueprint` to register widgets as extensions -- **Legacy System**: Use `createCardExtension` to export widgets as card components - -### New Frontend System +Homepage widgets are React components that can be added to customizable home pages. Create widgets using the `HomePageWidgetBlueprint`: @@ -156,26 +130,24 @@ const myWidget = HomePageWidgetBlueprint.make({ }); ``` -> **Example**: See [dev/index.tsx](dev/index.tsx) for a comprehensive example of creating multiple homepage widgets and layouts using the new frontend system. +> **Example**: See [dev/index.tsx](dev/index.tsx) for a comprehensive example of creating multiple homepage widgets and layouts. -### Legacy System - Widget Registration +## Contributing -In the legacy system, use the `createCardExtension` helper to create homepage widgets: +### Homepage Components -```tsx -import { createCardExtension } from '@backstage/plugin-home-react'; +We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for everyone to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components that can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) -export const MyWidget = homePlugin.provide( - createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ - title: 'My Custom Widget', - components: () => import('./homePageComponents/MyWidget'), - }), -); -``` +Additionally, the API is at a very early state, so contributing additional use cases may expose weaknesses in the current solution that we may iterate on to provide more flexibility and ease of use for those who wish to develop components for the Home Page. -The `createCardExtension` provides error boundary and lazy loading, and accepts generics for custom props that App Integrators can configure. +### Homepage Templates -## Legacy System Setup +We are hoping that we together can build up a collection of Homepage templates. We therefore put together a place where we can collect all the templates for the Home Plugin in the [storybook](https://backstage.io/storybook/?path=/story/plugins-home-templates). +If you would like to contribute with a template, start by taking a look at the [DefaultTemplate storybook example](/packages/app-legacy/src/components/home/templates/DefaultTemplate.stories.tsx) or [CustomizableTemplate storybook example](/packages/app-legacy/src/components/home/templates/CustomizableTemplate.stories.tsx) to create your own, and then open a PR with your suggestion. + +## Old Frontend System + +This section covers the home plugin setup and usage for apps that still use the old frontend system. ### Setting up the Home Page @@ -204,13 +176,26 @@ import { homePage } from './components/home/HomePage'; // ... ``` -### Creating Components (Legacy) +### Creating Widgets (Old Frontend System) -In the legacy system, homepage components can be regular React components or wrapped with `createCardExtension` for additional features like error boundaries and lazy loading. Components created with `createCardExtension` are exported as card components that can be composed into homepage layouts. +In the old frontend system, use the `createCardExtension` helper to create homepage widgets: -### Composing a Home Page (Legacy) +```tsx +import { createCardExtension } from '@backstage/plugin-home-react'; -In the legacy system, composing a Home Page is done by creating regular React components. Components created with `createCardExtension` are rendered like so: +export const MyWidget = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ + title: 'My Custom Widget', + components: () => import('./homePageComponents/MyWidget'), + }), +); +``` + +The `createCardExtension` provides error boundary and lazy loading, and accepts generics for custom props that App Integrators can configure. + +### Composing a Home Page (Old Frontend System) + +Composing a Home Page is done by creating regular React components. Components created with `createCardExtension` are rendered like so: ```tsx import Grid from '@material-ui/core/Grid'; @@ -227,7 +212,7 @@ export const homePage = ( Additionally, the App Integrator is provided an escape hatch in case the way the card is rendered does not fit their requirements. They may optionally pass the `Renderer`-prop, which will receive the `title`, `content` and optionally `actions`, `settings` and `contextProvider`, if they exist for the component. This allows the App Integrator to render the content in any way they want. -## Customizable home page +### Customizable Home Page (Old Frontend System) If you want to allow users to customize the components that are shown in the home page, you can use CustomHomePageGrid component. By adding the allowed components inside the grid, the user can add, configure, remove and move the components around in their @@ -259,7 +244,7 @@ export const homePage = ( > [!NOTE] > You can provide a title to the grid by passing it as a prop: ``. This will be displayed as a header above the grid layout. -### Creating Customizable Components +#### Creating Customizable Components (Old Frontend System) The custom home page can use the default components created by using the default `createCardExtension` method but if you want to add additional configuration like component size or settings, you can define those in the `layout` @@ -317,7 +302,7 @@ Available home page properties that are used for homepage widgets are: | `layout.height.maxRows` | integer | Maximum height of the widget (1-12) | | `settings.schema` | object | Customization settings of the widget, see below | -#### Widget Specific Settings +#### Widget Specific Settings (Old Frontend System) To define settings that the users can change for your component, you should define the `layout` and `settings` properties. The `settings.schema` object should follow @@ -369,7 +354,7 @@ Each widget has its own settings and the setting values are passed to the underl In case your `CardExtension` had `Settings` component defined, it will automatically disappear when you add the `settingsSchema` to the component data structure. -### Adding Default Layout +#### Adding Default Layout (Old Frontend System) You can set the default layout of the customizable home page by passing configuration to the `CustomHomepageGrid` component: @@ -391,7 +376,7 @@ const defaultConfig = [ ``` -## Page visit homepage component (HomePageTopVisited / HomePageRecentlyVisited) +### Page Visit Homepage Component (Old Frontend System) This component shows the homepage user a view for "Recently visited" or "Top visited". Being provided by the `` and `` component, see it in use on a homepage example below: @@ -505,11 +490,11 @@ home: In order to validate the config you can use `backstage/cli config:check` -### Customizing the VisitList +#### Customizing the VisitList (Old Frontend System) If you want more control over the recent and top visited lists, you can write your own functions to transform the path names and determine which visits to save. You can also enrich each visit with other fields and customize the chip colors/labels in the visit lists. -#### Transform Pathname Function +##### Transform Pathname Function Provide a `transformPathname` function to transform the pathname before it's processed for visit tracking. This can be used for transforming the pathname for the visit (before any other consideration). As an example, you can treat multiple sub-path visits to be counted as a singular path, e.g. `/entity-path/sub1` , `/entity-path/sub-2`, `/entity-path/sub-2/sub-sub-2` can all be mapped to `/entity-path` so visits to any of those routes are all counted as the same. @@ -548,7 +533,7 @@ export const apis: AnyApiFactory[] = [ ]; ``` -#### Can Save Function +##### Can Save Function Provide a `canSave` function to determine which visits should be tracked and saved. This allows you to conditionally save visits to the list: @@ -586,7 +571,7 @@ export const apis: AnyApiFactory[] = [ ]; ``` -#### Enrich Visit Function +##### Enrich Visit Function You can also add the `enrichVisit` function to put additional values on each `Visit`. The values could later be used to customize the chips in the `VisitList`. For example, you could add the entity `type` on the `Visit` so that `type` is used for labels instead of `kind`. @@ -637,7 +622,7 @@ export const apis: AnyApiFactory[] = [ ]; ``` -#### Custom Chip Colors and Labels +##### Custom Chip Colors and Labels To provide your own chip colors and/or labels for the recent and top visited lists, wrap the components in `VisitDisplayProvider` with `getChipColor` and `getChipLabel` functions. The colors provided will be used instead of the hard coded [`colorVariants`](https://github.com/backstage/backstage/blob/2da352043425bcab4c4422e4d2820c26c0a83382/packages/theme/src/base/pageTheme.ts#L46) provided via `@backstage/theme`. @@ -680,16 +665,3 @@ export default function HomePage() { ); } ``` - -## Contributing - -### Homepage Components - -We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for everyone to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components that can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) - -Additionally, the API is at a very early state, so contributing additional use cases may expose weaknesses in the current solution that we may iterate on to provide more flexibility and ease of use for those who wish to develop components for the Home Page. - -### Homepage Templates - -We are hoping that we together can build up a collection of Homepage templates. We therefore put together a place where we can collect all the templates for the Home Plugin in the [storybook](https://backstage.io/storybook/?path=/story/plugins-home-templates). -If you would like to contribute with a template, start by taking a look at the [DefaultTemplate storybook example](/packages/app-legacy/src/components/home/templates/DefaultTemplate.stories.tsx) or [CustomizableTemplate storybook example](/packages/app-legacy/src/components/home/templates/CustomizableTemplate.stories.tsx) to create your own, and then open a PR with your suggestion. diff --git a/plugins/home/package.json b/plugins/home/package.json index a89304174b..d35003d921 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.9.2-next.2", + "version": "0.9.3-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 4568fc8c2a..0269f471fe 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -14,6 +14,7 @@ import { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha'; import { HomePageWidgetBlueprintParams } from '@backstage/plugin-home-react/alpha'; import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -104,21 +105,67 @@ const _default: OverridableFrontendPlugin< 'page:home': OverridableExtensionDefinition<{ config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; widgets: ExtensionInput< ConfigurableExtensionDataRef< HomePageWidgetData, @@ -147,10 +194,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; } diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index 17870ed481..a90d5755ac 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -273,11 +273,9 @@ export interface VisitDisplayContextValue { } // @public -export const VisitDisplayProvider: ({ - children, - getChipColor, - getLabel, -}: VisitDisplayProviderProps) => JSX_2.Element; +export const VisitDisplayProvider: ( + input: VisitDisplayProviderProps, +) => JSX_2.Element; // @public export interface VisitDisplayProviderProps { @@ -309,14 +307,10 @@ export type VisitInput = { }; // @public -export const VisitListener: ({ - children, - toEntityRef, - visitName, -}: { +export const VisitListener: (input: { children?: ReactNode; - toEntityRef?: ({ pathname }: { pathname: string }) => string | undefined; - visitName?: ({ pathname }: { pathname: string }) => string; + toEntityRef?: (input: { pathname: string }) => string | undefined; + visitName?: (input: { pathname: string }) => string; }) => JSX.Element; // @public @@ -389,10 +383,7 @@ export type VisitsWebStorageApiOptions = { }; // @public -export const WelcomeTitle: ({ - language, - variant, -}: WelcomeTitleLanguageProps) => JSX_2.Element; +export const WelcomeTitle: (input: WelcomeTitleLanguageProps) => JSX_2.Element; // @public (undocumented) export type WelcomeTitleLanguageProps = { diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 23e9d0e42b..194f322317 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -63,6 +63,7 @@ const homePage = PageBlueprint.makeWithOverrides({ factory(originalFactory, { node, inputs }) { return originalFactory({ path: '/home', + noHeader: true, routeRef: rootRouteRef, loader: async () => { const LazyDefaultLayout = reactLazy(() => @@ -207,6 +208,8 @@ const homePageRandomJokeWidget = HomePageWidgetBlueprint.make({ */ export default createFrontendPlugin({ pluginId: 'home', + title: 'Home', + icon: , info: { packageJson: () => import('../package.json') }, extensions: [ homePage, diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 2856b6bae0..5d1231fc8d 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-kubernetes-backend +## 0.21.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-kubernetes-node@0.4.2-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 0.21.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.21.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.21.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-node@0.4.1 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + ## 0.21.1-next.2 ### Patch Changes diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index 74b2f57a31..7ab482822e 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -90,6 +90,13 @@ export interface Config { skipTLSVerify?: boolean; /** @visibility frontend */ skipMetricsLookup?: boolean; + /** + * The type of endpoint to use for connecting to the cluster. + * 'public' uses the public IP endpoint (default). + * 'dns' uses the DNS-based control plane endpoint. + * @visibility frontend + */ + endpointType?: 'public' | 'dns'; } >; customResources?: Array<{ diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 49c8759f4b..6f033b261b 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.21.1-next.2", + "version": "0.21.2-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts index 2b63814f62..b976532a47 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.test.ts @@ -17,6 +17,7 @@ import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { ConfigReader, Config } from '@backstage/config'; import { GkeClusterLocator } from './GkeClusterLocator'; +import { mockServices } from '@backstage/backend-test-utils'; import * as container from '@google-cloud/container'; const mockedListClusters = jest.fn(); @@ -31,8 +32,11 @@ jest.mock('@google-cloud/container', () => { }); describe('GkeClusterLocator', () => { + const logger = mockServices.logger.mock(); + beforeEach(() => { mockedListClusters.mockRestore(); + jest.clearAllMocks(); }); describe('config-parsing', () => { it('should accept missing region', async () => { @@ -41,9 +45,13 @@ describe('GkeClusterLocator', () => { projectId: 'some-project', }); - GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); expect(mockedListClusters).toHaveBeenCalledTimes(0); }); @@ -53,13 +61,34 @@ describe('GkeClusterLocator', () => { }); expect(() => - GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any), + GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ), ).toThrow("Missing required config value at 'projectId'"); expect(mockedListClusters).toHaveBeenCalledTimes(0); }); + it('should reject invalid endpointType', async () => { + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + endpointType: 'invalid', + }); + + expect(() => + GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ), + ).toThrow("Invalid endpointType 'invalid', must be one of: public, dns"); + }); }); describe('listClusters', () => { it('empty clusters returns empty cluster details', async () => { @@ -75,9 +104,13 @@ describe('GkeClusterLocator', () => { region: 'some-region', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -106,9 +139,13 @@ describe('GkeClusterLocator', () => { skipMetricsLookup: true, }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -143,9 +180,13 @@ describe('GkeClusterLocator', () => { projectId: 'some-project', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -185,9 +226,13 @@ describe('GkeClusterLocator', () => { region: 'some-region', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -240,9 +285,13 @@ describe('GkeClusterLocator', () => { region: 'some-region', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -301,9 +350,13 @@ describe('GkeClusterLocator', () => { ], }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -332,9 +385,13 @@ describe('GkeClusterLocator', () => { region: 'some-region', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); await expect(sut.getClusters()).rejects.toThrow( 'There was an error retrieving clusters from GKE for projectId=some-project region=some-region; caused by Error: some error', @@ -365,9 +422,13 @@ describe('GkeClusterLocator', () => { exposeDashboard: true, }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -408,9 +469,13 @@ describe('GkeClusterLocator', () => { projectId: 'some-project', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -442,9 +507,13 @@ describe('GkeClusterLocator', () => { authProvider: 'googleServiceAccount', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -478,9 +547,13 @@ describe('GkeClusterLocator', () => { authProvider: 'differentValue', }); - const sut = GkeClusterLocator.fromConfigWithClient(config, { - listClusters: mockedListClusters, - } as any); + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); const result = await sut.getClusters(); @@ -494,13 +567,99 @@ describe('GkeClusterLocator', () => { }, ]); }); + it('uses DNS endpoint when endpointType is dns', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + controlPlaneEndpointsConfig: { + dnsEndpointConfig: { + endpoint: 'gke-abc123.us-central1.gke.goog', + }, + }, + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + endpointType: 'dns', + }); + + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + name: 'some-cluster', + url: 'https://gke-abc123.us-central1.gke.goog', + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + }); + it('falls back to public IP with warning when endpointType is dns but no DNS endpoint available', async () => { + mockedListClusters.mockReturnValueOnce([ + { + clusters: [ + { + name: 'some-cluster', + endpoint: '1.2.3.4', + }, + ], + }, + ]); + + const config: Config = new ConfigReader({ + type: 'gke', + projectId: 'some-project', + region: 'some-region', + endpointType: 'dns', + }); + + const sut = GkeClusterLocator.fromConfigWithClient( + config, + { + listClusters: mockedListClusters, + } as any, + logger, + ); + + const result = await sut.getClusters(); + + expect(result).toStrictEqual([ + { + name: 'some-cluster', + url: 'https://1.2.3.4', + authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' }, + skipTLSVerify: false, + skipMetricsLookup: false, + }, + ]); + expect(logger.info).toHaveBeenCalledWith( + "Cluster 'some-cluster' has endpointType 'dns' configured but no DNS endpoint available, falling back to public IP", + ); + }); it('constructs ClusterManagerClient with identifying metadata', async () => { const configs: Config = new ConfigReader({ type: 'gke', projectId: 'some-project', }); - GkeClusterLocator.fromConfig(configs); + GkeClusterLocator.fromConfig(configs, logger); expect(container.v1.ClusterManagerClient).toHaveBeenCalledWith({ libName: 'backstage/kubernetes-backend.GkeClusterLocator', diff --git a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts index 2022d5f14a..844b1f8e82 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/GkeClusterLocator.ts @@ -24,6 +24,7 @@ import { ClusterDetails, KubernetesClustersSupplier, } from '@backstage/plugin-kubernetes-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; import packageinfo from '../../package.json'; interface MatchResourceLabelEntry { @@ -39,22 +40,28 @@ type GkeClusterLocatorOptions = { skipMetricsLookup?: boolean; exposeDashboard?: boolean; matchingResourceLabels?: MatchResourceLabelEntry[]; + endpointType?: 'public' | 'dns'; }; +const VALID_ENDPOINT_TYPES = ['public', 'dns'] as const; + export class GkeClusterLocator implements KubernetesClustersSupplier { private readonly options: GkeClusterLocatorOptions; private readonly client: container.v1.ClusterManagerClient; + private readonly logger: LoggerService; private clusterDetails: ClusterDetails[] | undefined; private hasClusterDetails: boolean; constructor( options: GkeClusterLocatorOptions, client: container.v1.ClusterManagerClient, + logger: LoggerService, clusterDetails: ClusterDetails[] | undefined = undefined, hasClusterDetails: boolean = false, ) { this.options = options; this.client = client; + this.logger = logger; this.clusterDetails = clusterDetails; this.hasClusterDetails = hasClusterDetails; } @@ -62,6 +69,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { static fromConfigWithClient( config: Config, client: container.v1.ClusterManagerClient, + logger: LoggerService, refreshInterval?: Duration, ): GkeClusterLocator { const matchingResourceLabels: MatchResourceLabelEntry[] = @@ -83,8 +91,9 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { config.getOptionalBoolean('skipMetricsLookup') ?? false, exposeDashboard: config.getOptionalBoolean('exposeDashboard') ?? false, matchingResourceLabels, + endpointType: parseEndpointType(config.getOptionalString('endpointType')), }; - const gkeClusterLocator = new GkeClusterLocator(options, client); + const gkeClusterLocator = new GkeClusterLocator(options, client, logger); if (refreshInterval) { runPeriodically( () => gkeClusterLocator.refreshClusters(), @@ -97,6 +106,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { // Added an `x-goog-api-client` header to API requests made by the GKE cluster locator to clearly identify API requests from this plugin. static fromConfig( config: Config, + logger: LoggerService, refreshInterval: Duration | undefined = undefined, ): GkeClusterLocator { return GkeClusterLocator.fromConfigWithClient( @@ -105,6 +115,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { libName: `backstage/kubernetes-backend.GkeClusterLocator`, libVersion: packageinfo.version, }), + logger, refreshInterval, ); } @@ -117,6 +128,24 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { return this.clusterDetails ?? []; } + private getClusterUrl( + cluster: container.protos.google.container.v1.ICluster, + ): string { + if (this.options.endpointType === 'dns') { + const dnsEndpoint = + cluster.controlPlaneEndpointsConfig?.dnsEndpointConfig?.endpoint; + if (dnsEndpoint) { + return `https://${dnsEndpoint}`; + } + this.logger.info( + `Cluster '${ + cluster.name ?? 'unknown' + }' has endpointType 'dns' configured but no DNS endpoint available, falling back to public IP`, + ); + } + return `https://${cluster.endpoint ?? ''}`; + } + // TODO pass caData into the object async refreshClusters(): Promise { const { @@ -146,7 +175,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { .map(r => ({ // TODO filter out clusters which don't have name or endpoint name: r.name ?? 'unknown', - url: `https://${r.endpoint ?? ''}`, + url: this.getClusterUrl(r), authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: authProvider }, skipTLSVerify, skipMetricsLookup, @@ -170,3 +199,25 @@ export class GkeClusterLocator implements KubernetesClustersSupplier { } } } + +function isValidEndpointType( + value: string, +): value is (typeof VALID_ENDPOINT_TYPES)[number] { + return VALID_ENDPOINT_TYPES.includes( + value as (typeof VALID_ENDPOINT_TYPES)[number], + ); +} + +function parseEndpointType(value: string | undefined): 'public' | 'dns' { + if (value === undefined) { + return 'public'; + } + if (isValidEndpointType(value)) { + return value; + } + throw new Error( + `Invalid endpointType '${value}', must be one of: ${VALID_ENDPOINT_TYPES.join( + ', ', + )}`, + ); +} diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index c18ff035e7..8437c81f45 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -101,6 +101,7 @@ export const getCombinedClusterSupplier = ( case 'gke': return GkeClusterLocator.fromConfig( clusterLocatorMethod, + logger, refreshInterval, ); default: diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 7f74838b61..7c3da576c6 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.0.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.0.34 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-react@0.5.16 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + ## 0.0.34-next.2 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 4fd69e7c4a..090a721323 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.34-next.2", + "version": "0.0.35-next.1", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 2b3308d6d4..237892a765 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-common +## 0.9.10 + +### Patch Changes + +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + ## 0.9.10-next.1 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 7f77a85ef0..e00fa68327 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.9.10-next.1", + "version": "0.9.10", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 696fb23363..b706200397 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-kubernetes-node +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## 0.4.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-kubernetes-common@0.9.10 + ## 0.4.1-next.1 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index f6bf681444..c8e1263ffd 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.4.1-next.1", + "version": "0.4.2-next.1", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index eca3af4405..bc698a616c 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-kubernetes-react +## 0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## 0.5.16 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- ce3639c: Add PersistentVolume and PersistentVolumeClaims Rendering +- d56542c: Updated dependency `@xterm/addon-attach` to `^0.12.0`. + Updated dependency `@xterm/addon-fit` to `^0.11.0`. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/core-plugin-api@1.12.3 + ## 0.5.16-next.2 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 30ce0302bb..abed58b6e7 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.5.16-next.2", + "version": "0.5.17-next.0", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes-react/report.api.md b/plugins/kubernetes-react/report.api.md index 497c8673c2..bb8057b2e8 100644 --- a/plugins/kubernetes-react/report.api.md +++ b/plugins/kubernetes-react/report.api.md @@ -61,10 +61,7 @@ export class AksKubernetesAuthProvider implements KubernetesAuthProvider { } // @public -export const Cluster: ({ - clusterObjects, - podsWithErrors, -}: ClusterProps) => JSX_2.Element; +export const Cluster: (input: ClusterProps) => JSX_2.Element; // @public (undocumented) export const ClusterContext: Context; @@ -116,7 +113,9 @@ export interface ContainerScope extends PodScope { } // @public (undocumented) -export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => JSX_2.Element; +export const CronJobsAccordions: ( + input: CronJobsAccordionsProps, +) => JSX_2.Element; // @public (undocumented) export type CronJobsAccordionsProps = { @@ -124,7 +123,7 @@ export type CronJobsAccordionsProps = { }; // @public (undocumented) -export const CustomResources: ({}: CustomResourcesProps) => JSX_2.Element; +export const CustomResources: (input: CustomResourcesProps) => JSX_2.Element; // @public (undocumented) export interface CustomResourcesProps { @@ -145,7 +144,7 @@ export class EksClusterLinksFormatter implements ClusterLinksFormatter { } // @public -export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX_2.Element; +export const ErrorList: (input: ErrorListProps) => JSX_2.Element; // @public export interface ErrorListProps { @@ -159,11 +158,7 @@ export type ErrorMatcher = { } & TypeMeta; // @public (undocumented) -export const ErrorPanel: ({ - entityName, - errorMessage, - clustersWithErrors, -}: ErrorPanelProps) => JSX_2.Element; +export const ErrorPanel: (input: ErrorPanelProps) => JSX_2.Element; // @public (undocumented) export type ErrorPanelProps = { @@ -174,10 +169,7 @@ export type ErrorPanelProps = { }; // @public (undocumented) -export const ErrorReporting: ({ - detectedErrors, - clusters, -}: ErrorReportingProps) => JSX_2.Element; +export const ErrorReporting: (input: ErrorReportingProps) => JSX_2.Element; // @public (undocumented) export type ErrorReportingProps = { @@ -186,18 +178,10 @@ export type ErrorReportingProps = { }; // @public -export const Events: ({ - involvedObjectName, - namespace, - clusterName, - warningEventsOnly, -}: EventsProps) => JSX_2.Element; +export const Events: (input: EventsProps) => JSX_2.Element; // @public -export const EventsContent: ({ - events, - warningEventsOnly, -}: EventsContentProps) => JSX_2.Element; +export const EventsContent: (input: EventsContentProps) => JSX_2.Element; // @public export interface EventsContentProps { @@ -297,13 +281,15 @@ export const HorizontalPodAutoscalerDrawer: (props: { }) => JSX_2.Element; // @public (undocumented) -export const IngressesAccordions: ({}: IngressesAccordionsProps) => JSX_2.Element; +export const IngressesAccordions: ( + input: IngressesAccordionsProps, +) => JSX_2.Element; // @public (undocumented) export type IngressesAccordionsProps = {}; // @public (undocumented) -export const JobsAccordions: ({ jobs }: JobsAccordionsProps) => JSX_2.Element; +export const JobsAccordions: (input: JobsAccordionsProps) => JSX_2.Element; // @public (undocumented) export type JobsAccordionsProps = { @@ -468,13 +454,7 @@ export interface KubernetesClusterLinkFormatterApi { export const kubernetesClusterLinkFormatterApiRef: ApiRef; // @public -export const KubernetesDrawer: ({ - open, - label, - drawerContentsHeader, - kubernetesObject, - children, -}: KubernetesDrawerProps) => JSX_2.Element; +export const KubernetesDrawer: (input: KubernetesDrawerProps) => JSX_2.Element; // @public (undocumented) export interface KubernetesDrawerable { @@ -549,11 +529,7 @@ export const kubernetesProxyApiRef: ApiRef; export class KubernetesProxyClient { constructor(options: { kubernetesApi: KubernetesApi }); // (undocumented) - deletePod({ - podName, - namespace, - clusterName, - }: { + deletePod(input: { podName: string; namespace: string; clusterName: string; @@ -561,23 +537,13 @@ export class KubernetesProxyClient { text: string; }>; // (undocumented) - getEventsByInvolvedObjectName({ - clusterName, - involvedObjectName, - namespace, - }: { + getEventsByInvolvedObjectName(input: { clusterName: string; involvedObjectName: string; namespace: string; }): Promise; // (undocumented) - getPodLogs({ - podName, - namespace, - clusterName, - containerName, - previous, - }: { + getPodLogs(input: { podName: string; namespace: string; clusterName: string; @@ -591,14 +557,9 @@ export class KubernetesProxyClient { // @public (undocumented) export const KubernetesStructuredMetadataTableDrawer: < T extends KubernetesDrawerable, ->({ - object, - renderObject, - kind, - buttonVariant, - expanded, - children, -}: KubernetesStructuredMetadataTableDrawerProps) => JSX_2.Element; +>( + input: KubernetesStructuredMetadataTableDrawerProps, +) => JSX_2.Element; // @public (undocumented) export interface KubernetesStructuredMetadataTableDrawerProps< @@ -619,10 +580,7 @@ export interface KubernetesStructuredMetadataTableDrawerProps< } // @public (undocumented) -export const LinkErrorPanel: ({ - cluster, - errorMessage, -}: LinkErrorPanelProps) => JSX_2.Element; +export const LinkErrorPanel: (input: LinkErrorPanelProps) => JSX_2.Element; // @public (undocumented) export type LinkErrorPanelProps = { @@ -632,7 +590,7 @@ export type LinkErrorPanelProps = { }; // @public -export const ManifestYaml: ({ object }: ManifestYamlProps) => JSX_2.Element; +export const ManifestYaml: (input: ManifestYamlProps) => JSX_2.Element; // @public export interface ManifestYamlProps { @@ -664,9 +622,9 @@ export class OpenshiftClusterLinksFormatter { } // @public -export const PendingPodContent: ({ - pod, -}: PendingPodContentProps) => JSX_2.Element; +export const PendingPodContent: ( + input: PendingPodContentProps, +) => JSX_2.Element; // @public export interface PendingPodContentProps { @@ -688,10 +646,7 @@ export interface PodAndErrors { export type PodColumns = 'READY' | 'RESOURCE'; // @public -export const PodDrawer: ({ - podAndErrors, - open, -}: PodDrawerProps) => JSX_2.Element; +export const PodDrawer: (input: PodDrawerProps) => JSX_2.Element; // @public export interface PodDrawerProps { @@ -725,9 +680,7 @@ export interface PodExecTerminalProps { export const PodLogs: FC; // @public -export const PodLogsDialog: ({ - containerScope, -}: PodLogsDialogProps) => JSX_2.Element; +export const PodLogsDialog: (input: PodLogsDialogProps) => JSX_2.Element; // @public export interface PodLogsDialogProps { @@ -776,10 +729,7 @@ export interface PodScope { } // @public (undocumented) -export const PodsTable: ({ - pods, - extraColumns, -}: PodsTablesProps) => JSX_2.Element; +export const PodsTable: (input: PodsTablesProps) => JSX_2.Element; // @public (undocumented) export type PodsTablesProps = { @@ -801,13 +751,9 @@ export const READY_COLUMNS: PodColumns; export const RESOURCE_COLUMNS: PodColumns; // @public -export const ResourceUtilization: ({ - compressed, - title, - usage, - total, - totalFormatted, -}: ResourceUtilizationProps) => JSX_2.Element; +export const ResourceUtilization: ( + input: ResourceUtilizationProps, +) => JSX_2.Element; // @public export interface ResourceUtilizationProps { @@ -836,7 +782,9 @@ export class ServerSideKubernetesAuthProvider } // @public (undocumented) -export const ServicesAccordions: ({}: ServicesAccordionsProps) => JSX_2.Element; +export const ServicesAccordions: ( + input: ServicesAccordionsProps, +) => JSX_2.Element; // @public (undocumented) export type ServicesAccordionsProps = {}; @@ -855,11 +803,7 @@ export const useCustomResources: ( ) => KubernetesObjects; // @public -export const useEvents: ({ - involvedObjectName, - namespace, - clusterName, -}: EventsOptions) => AsyncState; +export const useEvents: (input: EventsOptions) => AsyncState; // @public (undocumented) export const useKubernetesObjects: ( @@ -871,10 +815,7 @@ export const useKubernetesObjects: ( export const useMatchingErrors: (matcher: ErrorMatcher) => DetectedError[]; // @public -export const usePodLogs: ({ - containerScope, - previous, -}: PodLogsOptions) => AsyncState<{ +export const usePodLogs: (input: PodLogsOptions) => AsyncState<{ text: string; }>; diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 7b9fb95c7d..50ddf16295 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-kubernetes +## 0.12.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 0.12.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.12.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.12.16 + +### Patch Changes + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-kubernetes-react@0.5.16 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + ## 0.12.16-next.2 ### Patch Changes diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index e2c967902b..fe64aea257 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -8,15 +8,20 @@ It will elevate the visibility of errors where identified, and provide drill dow It directly interfaces with the [Kubernetes Backend Plugin (`@backstage-plugin-kubernetes-backend`)](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend). -_This plugin was created through the Backstage CLI_ - ## Introduction See our announcement blog post [New Backstage feature: Kubernetes for Service Owners](https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners) to learn more about the motivation behind developing the plugin. ## Setup & Configuration -This plugin must be explicitly added to a Backstage app, along with it's peer backend plugin. +This plugin must be installed in a Backstage app, along with its peer backend plugin. + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-kubernetes +``` + +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). It requires configuration in the Backstage `app-config.yaml` to connect to a Kubernetes API control plane. @@ -24,35 +29,9 @@ In addition, configuration of an entity's `catalog-info.yaml` helps identify whi For more information, see the [formal documentation about the Kubernetes feature in Backstage](https://backstage.io/docs/features/kubernetes/overview). -## Getting started +### Enabling the entity content tab -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 [/kubernetes](http://localhost:3000/catalog/default/component/:component-name/kubernetes). - -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. - -### Integrating with `EntityPage` (New Frontend System) - -Follow this section if you are using Backstage's [new frontend system](https://backstage.io/docs/frontend-system/). - -1. Import `kubernetesPlugin` in your `App.tsx` and add it to your app's `features` array: - -```typescript -import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; - -// ... - -export const app = createApp({ - features: [ - // ... - kubernetesPlugin, - // ... - ], -}); -``` - -2. Next, enable your desired extensions in `app-config.yaml`. +Enable the Kubernetes entity content extension in your `app-config.yaml`: ```yaml app: @@ -77,3 +56,11 @@ app: - resource - system ``` + +## 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 [/kubernetes](http://localhost:3000/catalog/default/component/:component-name/kubernetes). + +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/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index f2c051cd8f..d91b98debe 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.16-next.2", + "version": "0.12.17-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 46a576191f..7086976724 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -6,11 +6,14 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { defaultEntityContentGroups } from '@backstage/plugin-catalog-react/alpha'; import { Entity } from '@backstage/catalog-model'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { FilterPredicate } from '@backstage/filter-predicates'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSXElementConstructor } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -106,7 +109,6 @@ const _default: OverridableFrontendPlugin< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -114,6 +116,7 @@ const _default: OverridableFrontendPlugin< optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -162,26 +165,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index be0a2cfbf0..cc169b360c 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.10-next.2 + +### Patch Changes + +- c74b697: Added support for splitting MCP actions into multiple servers via `mcpActions.servers` configuration. Each server gets its own endpoint at `/api/mcp-actions/v1/{key}` with actions scoped using include/exclude filter rules. Tool names are now namespaced with the plugin ID by default, configurable via `mcpActions.namespacedToolNames`. When `mcpActions.servers` is not configured, the plugin continues to serve a single server at `/api/mcp-actions/v1`. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 0.1.10-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.10-next.0 + +### Patch Changes + +- dc81af1: Adds two new metrics to track MCP server operations and sessions. + + - `mcp.server.operation.duration`: The duration taken to process an individual MCP operation + - `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.1.9 + +### Patch Changes + +- 31de2c9: Added OAuth Protected Resource Metadata endpoint (`/.well-known/oauth-protected-resource`) per RFC 9728. This allows MCP clients to discover the authorization server for the resource. + + Also enabled OAuth well-known endpoints when CIMD (Client ID Metadata Documents) is configured, not just when DCR is enabled. + +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index c2eba60c80..d496cf0f03 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -71,11 +71,69 @@ export const myPlugin = createBackendPlugin({ }); ``` +### Namespaced Tool Names + +By default, MCP tool names include the plugin ID prefix to avoid collisions across plugins. For example, an action registered as `greet-user` by `my-custom-plugin` is exposed as `my-custom-plugin.greet-user`. + +You can disable this if you need the short names for backward compatibility: + +```yaml +mcpActions: + namespacedToolNames: false +``` + +### Multiple MCP Servers + +By default, the plugin serves a single MCP server at `/api/mcp-actions/v1` that exposes all available actions. You can split actions into multiple focused servers by configuring `mcpActions.servers`, where each key becomes a separate MCP server endpoint. + +```yaml +mcpActions: + servers: + catalog: + name: 'Backstage Catalog' + description: 'Tools for interacting with the software catalog' + filter: + include: + - id: 'catalog:*' + scaffolder: + name: 'Backstage Scaffolder' + description: 'Tools for creating new software from templates' + filter: + include: + - id: 'scaffolder:*' +``` + +This creates two MCP server endpoints: + +- `http://localhost:7007/api/mcp-actions/v1/catalog` +- `http://localhost:7007/api/mcp-actions/v1/scaffolder` + +Each server uses include filter rules with glob patterns on action IDs to control which actions are exposed. For example, `id: 'catalog:*'` matches all actions registered by the catalog plugin. + +When `mcpActions.servers` is not configured, the plugin behaves exactly as before with a single server at `/api/mcp-actions/v1`. + +#### Filter Rules + +Include and exclude filter rules support glob patterns on action IDs and attribute matching. Exclude rules take precedence over include rules. When include rules are specified, actions must match at least one include rule to be exposed. + +```yaml +mcpActions: + servers: + catalog: + name: 'Backstage Catalog' + filter: + include: + - id: 'catalog:*' + exclude: + - attributes: + destructive: true +``` + ### Error Handling When errors are thrown from MCP actions, the backend will handle and surface error message for any error from `@backstage/errors`. Unknown errors will be handled by `@modelcontextprotocol/sdk`'s default error handling, which may result in a generic `500 Server Error` being returned. As a result, we recommend using errors from `@backstage/errors` when applicable. -See https://backstage.io/api/stable/modules/_backstage_errors.html for a full list of supported errors. +See [Backstage Errors](https://backstage.io/docs/reference/errors/) for a full list of supported errors. When writing MCP tools, use the appropriate error from `@backstage/errors` when applicable: @@ -159,16 +217,15 @@ The MCP server supports both Server-Sent Events (SSE) and Streamable HTTP protoc The SSE protocol is deprecated, and should be avoided as it will be removed in a future release. +### Single Server (default) + - `Streamable HTTP`: `http://localhost:7007/api/mcp-actions/v1` - `SSE`: `http://localhost:7007/api/mcp-actions/v1/sse` -There's a few different ways to configure MCP tools, but here's a snippet of the most common. - ```json { "mcpServers": { "backstage-actions": { - // you can also replace this with the public / internal URL of the deployed backend. "url": "http://localhost:7007/api/mcp-actions/v1", "headers": { "Authorization": "Bearer ${MCP_TOKEN}" @@ -178,6 +235,39 @@ There's a few different ways to configure MCP tools, but here's a snippet of the } ``` +### Multiple Servers + +When `mcpActions.servers` is configured, each server key becomes part of the URL. For example, with servers named `catalog` and `scaffolder`: + +- `http://localhost:7007/api/mcp-actions/v1/catalog` +- `http://localhost:7007/api/mcp-actions/v1/scaffolder` + +```json +{ + "mcpServers": { + "backstage-catalog": { + "url": "http://localhost:7007/api/mcp-actions/v1/catalog", + "headers": { + "Authorization": "Bearer ${MCP_TOKEN}" + } + }, + "backstage-scaffolder": { + "url": "http://localhost:7007/api/mcp-actions/v1/scaffolder", + "headers": { + "Authorization": "Bearer ${MCP_TOKEN}" + } + } + } +} +``` + +## Metrics + +The MCP Actions Backend emits metrics for the following operations: + +- `mcp.server.operation.duration`: The duration taken to process an individual MCP operation +- `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server + ## Development This plugin backend can be started in a standalone mode from directly in this package with `yarn start`. It is a limited setup that is most convenient when developing the plugin backend itself. diff --git a/plugins/mcp-actions-backend/config.d.ts b/plugins/mcp-actions-backend/config.d.ts new file mode 100644 index 0000000000..a530f5b038 --- /dev/null +++ b/plugins/mcp-actions-backend/config.d.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2026 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 interface Config { + mcpActions?: { + /** + * Display name for the MCP server. Defaults to "backstage". + * Used when running a single bundled server without mcpActions.servers. + */ + name?: string; + + /** + * Description of the MCP server. + * Used when running a single bundled server without mcpActions.servers. + */ + description?: string; + + /** + * When true, MCP tool names include the plugin ID prefix to avoid + * collisions across plugins. For example an action registered as + * "get-entity" by the catalog plugin becomes "catalog.get-entity". + * Defaults to true. + */ + namespacedToolNames?: boolean; + + /** + * Named MCP servers, each exposed at /api/mcp-actions/v1/{key}. + * When not configured, the plugin serves a single server at /api/mcp-actions/v1. + */ + servers?: { + [serverKey: string]: { + /** Display name for the MCP server. */ + name: string; + /** Description of the MCP server. */ + description?: string; + /** Filter rules to include or exclude specific actions. */ + filter?: { + include?: Array<{ + /** Glob pattern matched against action ID. */ + id?: string; + /** Match actions by their attribute flags. */ + attributes?: { + destructive?: boolean; + readOnly?: boolean; + idempotent?: boolean; + }; + }>; + exclude?: Array<{ + /** Glob pattern matched against action ID. */ + id?: string; + /** Match actions by their attribute flags. */ + attributes?: { + destructive?: boolean; + readOnly?: boolean; + idempotent?: boolean; + }; + }>; + }; + }; + }; + }; +} diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 501a1d09e5..770e52474c 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.9-next.1", + "version": "0.1.10-next.2", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", @@ -21,8 +21,10 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "configSchema": "config.d.ts", "files": [ - "dist" + "dist", + "config.d.ts" ], "scripts": { "build": "backstage-cli package build", @@ -36,6 +38,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", @@ -43,12 +46,15 @@ "@modelcontextprotocol/sdk": "^1.25.2", "express": "^4.22.0", "express-promise-router": "^4.1.0", + "minimatch": "^10.2.1", "zod": "^3.25.76" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@types/express": "^4.17.6" + "@types/express": "^4.17.6", + "@types/supertest": "^2.0.8", + "supertest": "^7.0.0" } } diff --git a/plugins/mcp-actions-backend/src/config.ts b/plugins/mcp-actions-backend/src/config.ts new file mode 100644 index 0000000000..5ff2a442bb --- /dev/null +++ b/plugins/mcp-actions-backend/src/config.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2026 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 { Config } from '@backstage/config'; +import { Minimatch } from 'minimatch'; + +export type FilterRule = { + idMatcher?: Minimatch; + attributes?: Partial< + Record<'destructive' | 'readOnly' | 'idempotent', boolean> + >; +}; + +export type McpServerConfig = { + name: string; + description?: string; + includeRules: FilterRule[]; + excludeRules: FilterRule[]; +}; + +const SERVER_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + +export function parseFilterRules(configArray: Config[]): FilterRule[] { + return configArray.map(ruleConfig => { + const idPattern = ruleConfig.getOptionalString('id'); + const attributesConfig = ruleConfig.getOptionalConfig('attributes'); + + const rule: FilterRule = {}; + + if (idPattern) { + rule.idMatcher = new Minimatch(idPattern); + } + + if (attributesConfig) { + rule.attributes = {}; + for (const key of ['destructive', 'readOnly', 'idempotent'] as const) { + const value = attributesConfig.getOptionalBoolean(key); + if (value !== undefined) { + rule.attributes[key] = value; + } + } + } + + return rule; + }); +} + +export function parseServerConfigs( + config: Config, +): Map | undefined { + const serversConfig = config.getOptionalConfig('mcpActions.servers'); + if (!serversConfig) { + return undefined; + } + + const servers = new Map(); + + for (const key of serversConfig.keys()) { + if (!SERVER_KEY_PATTERN.test(key)) { + throw new Error( + `Invalid MCP server key "${key}": must be lowercase alphanumeric with hyphens`, + ); + } + + const serverConfig = serversConfig.getConfig(key); + + const filterConfig = serverConfig.getOptionalConfig('filter'); + const includeRules = parseFilterRules( + filterConfig?.getOptionalConfigArray('include') ?? [], + ); + const excludeRules = parseFilterRules( + filterConfig?.getOptionalConfigArray('exclude') ?? [], + ); + + servers.set(key, { + name: serverConfig.getString('name'), + description: serverConfig.getOptionalString('description'), + includeRules, + excludeRules, + }); + } + + return servers; +} diff --git a/plugins/mcp-actions-backend/src/metrics.ts b/plugins/mcp-actions-backend/src/metrics.ts new file mode 100644 index 0000000000..ddaa75edbd --- /dev/null +++ b/plugins/mcp-actions-backend/src/metrics.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2026 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 { MetricAttributes } from '@backstage/backend-plugin-api/alpha'; + +/** + * Attributes for mcp.server.operation.duration + * Following OTel requirement levels from the spec + * + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/#metric-mcpserveroperationduration + */ +export interface McpServerOperationAttributes extends MetricAttributes { + // Required + 'mcp.method.name': string; + + // Conditionally Required + 'error.type'?: string; + 'gen_ai.tool.name'?: string; + 'gen_ai.prompt.name'?: string; + 'mcp.resource.uri'?: string; + 'rpc.response.status_code'?: string; + + // Recommended + 'gen_ai.operation.name'?: 'execute_tool'; + 'mcp.protocol.version'?: string; + 'mcp.session.id'?: string; + 'network.transport'?: 'tcp' | 'quic' | 'pipe' | 'unix'; + 'network.protocol.name'?: string; + 'network.protocol.version'?: string; +} + +/** + * Attributes for mcp.server.session.duration + * Following OTel requirement levels from the spec + * + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/#metric-mcpserversessionduration + */ +export interface McpServerSessionAttributes extends MetricAttributes { + // Conditionally Required + 'error.type'?: string; + + // Recommended + 'mcp.protocol.version'?: string; + 'network.transport'?: 'tcp' | 'quic' | 'pipe' | 'unix'; + 'network.protocol.name'?: string; + 'network.protocol.version'?: string; +} + +/** + * OTel recommended bucket boundaries for MCP metrics + * + * @remarks + * + * Based on the MCP metrics defined in the OTel semantic conventions v1.39.0 + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/ + * + */ +export const bucketBoundaries = [ + 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, +]; diff --git a/plugins/mcp-actions-backend/src/plugin.test.ts b/plugins/mcp-actions-backend/src/plugin.test.ts index 47dd89c9fc..2c03800915 100644 --- a/plugins/mcp-actions-backend/src/plugin.test.ts +++ b/plugins/mcp-actions-backend/src/plugin.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; import { mcpPlugin } from './plugin'; import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { createBackendPlugin } from '@backstage/backend-plugin-api'; @@ -21,6 +22,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import request from 'supertest'; describe('Mcp Backend', () => { const mockPluginWithActions = createBackendPlugin({ @@ -51,6 +53,7 @@ describe('Mcp Backend', () => { features: [ mcpPlugin, mockPluginWithActions, + metricsServiceMock.mock().factory, mockServices.rootConfig.factory({ data: { backend: { @@ -115,7 +118,7 @@ describe('Mcp Backend', () => { required: ['name'], type: 'object', }, - name: 'make-greeting', + name: 'local.make-greeting', }, ]); }); @@ -158,8 +161,211 @@ describe('Mcp Backend', () => { required: ['name'], type: 'object', }, - name: 'make-greeting', + name: 'local.make-greeting', }, ]); }); + + describe('multi-server routing', () => { + const mockCatalogPlugin = createBackendPlugin({ + pluginId: 'catalog-actions', + register({ registerInit }) { + registerInit({ + deps: { actionsRegistry: actionsRegistryServiceRef }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'get-entity', + title: 'Get Entity', + description: 'Fetch an entity', + schema: { + input: z => z.object({ name: z.string() }), + output: z => z.object({ entity: z.string() }), + }, + action: async ({ input }) => ({ + output: { entity: input.name }, + }), + }); + }, + }); + }, + }); + + const mockScaffolderPlugin = createBackendPlugin({ + pluginId: 'scaffolder-actions', + register({ registerInit }) { + registerInit({ + deps: { actionsRegistry: actionsRegistryServiceRef }, + async init({ actionsRegistry }) { + actionsRegistry.register({ + name: 'create-app', + title: 'Create App', + description: 'Create an app from template', + schema: { + input: z => z.object({ template: z.string() }), + output: z => z.object({ name: z.string() }), + }, + action: async ({ input }) => ({ + output: { name: input.template }, + }), + }); + }, + }); + }, + }); + + it('should route to per-server endpoints when mcpActions.servers is configured', async () => { + const { server } = await startTestBackend({ + features: [ + mcpPlugin, + mockCatalogPlugin, + mockScaffolderPlugin, + metricsServiceMock.mock().factory, + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['catalog-actions', 'scaffolder-actions'], + }, + }, + mcpActions: { + servers: { + catalog: { + name: 'Catalog Server', + filter: { + include: [{ id: 'catalog-actions:*' }], + }, + }, + scaffolder: { + name: 'Scaffolder Server', + filter: { + include: [{ id: 'scaffolder-actions:*' }], + }, + }, + }, + }, + }, + }), + ], + }); + + const address = server.address(); + if (typeof address !== 'object' || !('port' in address!)) { + throw new Error('server broke'); + } + const serverAddress = `http://localhost:${address.port}`; + + const catalogClient = new Client({ name: 'test', version: '1.0' }); + const catalogTransport = new StreamableHTTPClientTransport( + new URL(`${serverAddress}/api/mcp-actions/v1/catalog`), + ); + await catalogClient.connect(catalogTransport); + const catalogResult = await catalogClient.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + expect(catalogResult.tools).toHaveLength(1); + expect(catalogResult.tools[0].name).toBe('catalog-actions.get-entity'); + + const scaffolderClient = new Client({ name: 'test', version: '1.0' }); + const scaffolderTransport = new StreamableHTTPClientTransport( + new URL(`${serverAddress}/api/mcp-actions/v1/scaffolder`), + ); + await scaffolderClient.connect(scaffolderTransport); + const scaffolderResult = await scaffolderClient.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + expect(scaffolderResult.tools).toHaveLength(1); + expect(scaffolderResult.tools[0].name).toBe( + 'scaffolder-actions.create-app', + ); + }); + }); + + describe('OAuth well-known endpoints', () => { + it('should not expose oauth endpoints when neither DCR nor CIMD is enabled', async () => { + const { server } = await startTestBackend({ + features: [ + mcpPlugin, + mockPluginWithActions, + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['local'], + }, + }, + }, + }), + ], + }); + + const response = await request(server).get( + '/.well-known/oauth-protected-resource', + ); + expect(response.status).toBe(404); + }); + + it('should expose oauth-protected-resource when DCR is enabled', async () => { + const { server } = await startTestBackend({ + features: [ + mcpPlugin, + mockPluginWithActions, + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['local'], + }, + }, + auth: { + experimentalDynamicClientRegistration: { + enabled: true, + }, + }, + }, + }), + ], + }); + + const response = await request(server).get( + '/.well-known/oauth-protected-resource', + ); + expect(response.status).toBe(200); + expect(response.body.resource).toMatch(/\/api\/mcp-actions$/); + expect(response.body.authorization_servers).toHaveLength(1); + expect(response.body.authorization_servers[0]).toMatch(/\/api\/auth$/); + }); + + it('should expose oauth-protected-resource when CIMD is enabled', async () => { + const { server } = await startTestBackend({ + features: [ + mcpPlugin, + mockPluginWithActions, + mockServices.rootConfig.factory({ + data: { + backend: { + actions: { + pluginSources: ['local'], + }, + }, + auth: { + experimentalClientIdMetadataDocuments: { + enabled: true, + }, + }, + }, + }), + ], + }); + + const response = await request(server).get( + '/.well-known/oauth-protected-resource', + ); + expect(response.status).toBe(200); + expect(response.body.resource).toMatch(/\/api\/mcp-actions$/); + expect(response.body.authorization_servers).toHaveLength(1); + expect(response.body.authorization_servers[0]).toMatch(/\/api\/auth$/); + }); + }); }); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index d04df6cdf5..f8844249f6 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -25,7 +25,9 @@ import { createSseRouter } from './routers/createSseRouter'; import { actionsRegistryServiceRef, actionsServiceRef, + metricsServiceRef, } from '@backstage/backend-plugin-api/alpha'; +import { parseServerConfigs } from './config'; /** * mcpPlugin backend plugin @@ -46,6 +48,7 @@ export const mcpPlugin = createBackendPlugin({ rootRouter: coreServices.rootHttpRouter, discovery: coreServices.discovery, config: coreServices.rootConfig, + metrics: metricsServiceRef, }, async init({ actions, @@ -55,37 +58,74 @@ export const mcpPlugin = createBackendPlugin({ rootRouter, discovery, config, + metrics, }) { + const serverConfigs = parseServerConfigs(config); + const namespacedToolNames = config.getOptionalBoolean( + 'mcpActions.namespacedToolNames', + ); + const mcpService = await McpService.create({ actions, - }); - - const sseRouter = createSseRouter({ - mcpService, - httpAuth, - }); - - const streamableRouter = createStreamableRouter({ - mcpService, - httpAuth, - logger, + metrics, + namespacedToolNames, }); const router = Router(); router.use(json()); - router.use('/v1/sse', sseRouter); - router.use('/v1', streamableRouter); + if (serverConfigs && serverConfigs.size > 0) { + for (const [key, serverConfig] of serverConfigs) { + const streamableRouter = createStreamableRouter({ + mcpService, + httpAuth, + logger, + metrics, + serverConfig, + }); + + router.use(`/v1/${key}`, streamableRouter); + } + } else { + const serverConfig = { + name: config.getOptionalString('mcpActions.name') ?? 'backstage', + description: config.getOptionalString('mcpActions.description'), + includeRules: [], + excludeRules: [], + }; + + const sseRouter = createSseRouter({ + mcpService, + httpAuth, + serverConfig, + }); + + const streamableRouter = createStreamableRouter({ + mcpService, + httpAuth, + logger, + metrics, + serverConfig, + }); + + router.use('/v1/sse', sseRouter); + router.use('/v1', streamableRouter); + } httpRouter.use(router); - if ( + const oauthEnabled = config.getOptionalBoolean( 'auth.experimentalDynamicClientRegistration.enabled', - ) - ) { + ) || + config.getOptionalBoolean( + 'auth.experimentalClientIdMetadataDocuments.enabled', + ); + + if (oauthEnabled) { + // OAuth Authorization Server Metadata (RFC 8414) // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by - // many of the MCP client as of yet. So this seems to be the oldest version of the spec thats implemented. + // many of the MCP clients as of yet. So this seems to be the oldest version of the spec that's implemented. rootRouter.use( '/.well-known/oauth-authorization-server', async (_, res) => { @@ -93,10 +133,26 @@ export const mcpPlugin = createBackendPlugin({ const oidcResponse = await fetch( `${authBaseUrl}/.well-known/openid-configuration`, ); - res.json(await oidcResponse.json()); }, ); + + // Protected Resource Metadata (RFC 9728) + // https://datatracker.ietf.org/doc/html/rfc9728 + // This allows MCP clients to discover the authorization server for this resource + rootRouter.use( + '/.well-known/oauth-protected-resource', + async (_, res) => { + const [authBaseUrl, mcpBaseUrl] = await Promise.all([ + discovery.getBaseUrl('auth'), + discovery.getBaseUrl('mcp-actions'), + ]); + res.json({ + resource: mcpBaseUrl, + authorization_servers: [authBaseUrl], + }); + }, + ); } }, }); diff --git a/plugins/mcp-actions-backend/src/routers/createSseRouter.ts b/plugins/mcp-actions-backend/src/routers/createSseRouter.ts index 19bcf288e9..954d28af4e 100644 --- a/plugins/mcp-actions-backend/src/routers/createSseRouter.ts +++ b/plugins/mcp-actions-backend/src/routers/createSseRouter.ts @@ -18,6 +18,7 @@ import { Router } from 'express'; import { McpService } from '../services/McpService'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { HttpAuthService } from '@backstage/backend-plugin-api'; +import { McpServerConfig } from '../config'; /** * Legacy SSE endpoint for older clients, hopefully will not be needed for much longer. @@ -25,9 +26,11 @@ import { HttpAuthService } from '@backstage/backend-plugin-api'; export const createSseRouter = ({ mcpService, httpAuth, + serverConfig, }: { mcpService: McpService; httpAuth: HttpAuthService; + serverConfig?: McpServerConfig; }): Router => { const router = PromiseRouter(); const transportsToSessionId = new Map(); @@ -35,6 +38,7 @@ export const createSseRouter = ({ router.get('/', async (req, res) => { const server = mcpService.getServer({ credentials: await httpAuth.credentials(req), + serverConfig, }); const transport = new SSEServerTransport( diff --git a/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts b/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts index 9d6fd1840b..0225bbb98d 100644 --- a/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts +++ b/plugins/mcp-actions-backend/src/routers/createStreamableRouter.ts @@ -15,26 +15,54 @@ */ import PromiseRouter from 'express-promise-router'; import { Router } from 'express'; +import { performance } from 'node:perf_hooks'; import { McpService } from '../services/McpService'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js'; import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api'; import { isError } from '@backstage/errors'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; +import { bucketBoundaries, McpServerSessionAttributes } from '../metrics'; +import { McpServerConfig } from '../config'; export const createStreamableRouter = ({ mcpService, httpAuth, logger, + metrics, + serverConfig, }: { mcpService: McpService; logger: LoggerService; httpAuth: HttpAuthService; + metrics: MetricsService; + serverConfig?: McpServerConfig; }): Router => { const router = PromiseRouter(); + const sessionDuration = metrics.createHistogram( + 'mcp.server.session.duration', + { + description: + 'The duration of the MCP session as observed on the MCP server', + unit: 's', + advice: { explicitBucketBoundaries: bucketBoundaries }, + }, + ); + router.post('/', async (req, res) => { + const sessionStart = performance.now(); + + const baseAttributes: McpServerSessionAttributes = { + 'mcp.protocol.version': LATEST_PROTOCOL_VERSION, + 'network.transport': 'tcp', + 'network.protocol.name': 'http', + }; + try { const server = mcpService.getServer({ credentials: await httpAuth.credentials(req), + serverConfig, }); const transport = new StreamableHTTPServerTransport({ @@ -49,8 +77,14 @@ export const createStreamableRouter = ({ res.on('close', () => { transport.close(); server.close(); + + const durationSeconds = (performance.now() - sessionStart) / 1000; + + sessionDuration.record(durationSeconds, baseAttributes); }); } catch (error) { + const errorType = isError(error) ? error.name : 'Error'; + if (isError(error)) { logger.error(error.message); } @@ -65,6 +99,13 @@ export const createStreamableRouter = ({ id: null, }); } + + const durationSeconds = (performance.now() - sessionStart) / 1000; + + sessionDuration.record(durationSeconds, { + ...baseAttributes, + 'error.type': errorType, + }); } }); diff --git a/plugins/mcp-actions-backend/src/services/McpService.test.ts b/plugins/mcp-actions-backend/src/services/McpService.test.ts index cd1c98f5b1..1a7dc507c3 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.test.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.test.ts @@ -16,13 +16,20 @@ import { mockCredentials } from '@backstage/backend-test-utils'; import { McpService } from './McpService'; -import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { + actionsRegistryServiceMock, + metricsServiceMock, +} from '@backstage/backend-test-utils/alpha'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { CallToolResultSchema, ListToolsResultSchema, } from '@modelcontextprotocol/sdk/types.js'; +import { InputError, NotFoundError } from '@backstage/errors'; +import { McpServerConfig, parseFilterRules } from '../config'; +import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { ConfigReader } from '@backstage/config'; describe('McpService', () => { it('should list the available actions as tools in the mcp backend', async () => { @@ -38,8 +45,10 @@ describe('McpService', () => { action: async () => ({ output: { output: 'test' } }), }); + const mockMetrics = metricsServiceMock.mock(); const mcpService = await McpService.create({ actions: mockActionsRegistry, + metrics: mockMetrics, }); const server = mcpService.getServer({ @@ -87,9 +96,63 @@ describe('McpService', () => { required: ['input'], type: 'object', }, - name: 'mock-action', + name: 'test.mock-action', }, ]); + + const histogram = mockMetrics.createHistogram.mock.results[0]?.value; + expect(histogram.record).toHaveBeenCalledTimes(1); + expect(histogram.record).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + 'mcp.method.name': 'tools/list', + }), + ); + expect(histogram.record.mock.calls[0][1]).not.toHaveProperty('error.type'); + }); + + it('should record metrics with error.type when tools/list fails', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.list = jest + .fn() + .mockRejectedValue(new Error('List failed')); + + const mockMetrics = metricsServiceMock.mock(); + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: mockMetrics, + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ + name: 'test client', + version: '1.0', + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + await expect( + client.request({ method: 'tools/list' }, ListToolsResultSchema), + ).rejects.toThrow(); + + const histogram = mockMetrics.createHistogram.mock.results[0]?.value; + expect(histogram.record).toHaveBeenCalledTimes(1); + expect(histogram.record).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + 'mcp.method.name': 'tools/list', + 'error.type': 'Error', + }), + ); }); it('should call the action when the tool is invoked', async () => { @@ -107,8 +170,10 @@ describe('McpService', () => { action: mockAction, }); + const mockMetrics = metricsServiceMock.mock(); const mcpService = await McpService.create({ actions: mockActionsRegistry, + metrics: mockMetrics, }); const server = mcpService.getServer({ @@ -131,7 +196,7 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'mock-action', arguments: { input: 'test' } }, + params: { name: 'test.mock-action', arguments: { input: 'test' } }, }, CallToolResultSchema, ); @@ -154,11 +219,25 @@ describe('McpService', () => { ].join('\n'), }, ]); + + const histogram = mockMetrics.createHistogram.mock.results[0]?.value; + expect(histogram.record).toHaveBeenCalledTimes(1); + expect(histogram.record).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + 'mcp.method.name': 'tools/call', + 'gen_ai.tool.name': 'test.mock-action', + 'gen_ai.operation.name': 'execute_tool', + }), + ); + expect(histogram.record.mock.calls[0][1]).not.toHaveProperty('error.type'); }); it('should return an error when the action is not found', async () => { + const mockMetrics = metricsServiceMock.mock(); const mcpService = await McpService.create({ actions: actionsRegistryServiceMock(), + metrics: mockMetrics, }); const server = mcpService.getServer({ @@ -181,18 +260,651 @@ describe('McpService', () => { const result = await client.request( { method: 'tools/call', - params: { name: 'mock-action', arguments: { input: 'test' } }, + params: { name: 'nonexistent-action', arguments: { input: 'test' } }, }, CallToolResultSchema, ); await expect(result).toEqual({ content: [ { - text: expect.stringMatching('Action "mock-action" not found'), + text: expect.stringMatching('Action "nonexistent-action" not found'), type: 'text', }, ], isError: true, }); + + const histogram = mockMetrics.createHistogram.mock.results[0]?.value; + expect(histogram.record).toHaveBeenCalledTimes(1); + expect(histogram.record).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + 'mcp.method.name': 'tools/call', + 'gen_ai.tool.name': 'nonexistent-action', + 'gen_ai.operation.name': 'execute_tool', + 'error.type': 'tool_error', + }), + ); + }); + + it('should record metrics with error.type when tool invocation throws', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const customError = new Error('Action failed'); + customError.name = 'CustomError'; + mockActionsRegistry.register({ + name: 'failing-action', + title: 'Failing', + description: 'Fails', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: jest.fn().mockRejectedValue(customError), + }); + + const mockMetrics = metricsServiceMock.mock(); + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: mockMetrics, + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ + name: 'test client', + version: '1.0', + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + await expect( + client.request( + { + method: 'tools/call', + params: { name: 'test.failing-action', arguments: {} }, + }, + CallToolResultSchema, + ), + ).rejects.toThrow('Action failed'); + + const histogram = mockMetrics.createHistogram.mock.results[0]?.value; + expect(histogram.record).toHaveBeenCalledTimes(1); + expect(histogram.record).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + 'mcp.method.name': 'tools/call', + 'gen_ai.tool.name': 'test.failing-action', + 'gen_ai.operation.name': 'execute_tool', + 'error.type': 'CustomError', + }), + ); + }); + + it('should forward the original InputError when an action throws one', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'failing-action', + title: 'Failing', + description: 'An action that throws InputError', + schema: { + input: z => z.object({ value: z.string() }), + output: z => z.object({}), + }, + action: async () => { + throw new InputError('the value was invalid'); + }, + }); + + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ + name: 'test client', + version: '1.0', + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: 'tools/call', + params: { name: 'test.failing-action', arguments: { value: 'test' } }, + }, + CallToolResultSchema, + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: 'InputError: the value was invalid', + }, + ], + isError: true, + }); + }); + + it('should forward the original NotFoundError when an action throws one', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'not-found-action', + title: 'Not Found', + description: 'An action that throws NotFoundError', + schema: { + input: z => z.object({ id: z.string() }), + output: z => z.object({}), + }, + action: async () => { + throw new NotFoundError('entity does not exist'); + }, + }); + + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ + name: 'test client', + version: '1.0', + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: 'tools/call', + params: { name: 'test.not-found-action', arguments: { id: 'abc' } }, + }, + CallToolResultSchema, + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: 'NotFoundError: entity does not exist', + }, + ], + isError: true, + }); + }); + + describe('per-server filtering', () => { + const fakeActions = [ + { + id: 'catalog:get-entity', + pluginId: 'catalog', + name: 'get-entity', + title: 'Get Entity', + description: 'Fetch an entity', + schema: { + input: { type: 'object' as const }, + output: { type: 'object' as const }, + }, + attributes: { destructive: false, readOnly: true, idempotent: true }, + }, + { + id: 'catalog:delete-entity', + pluginId: 'catalog', + name: 'delete-entity', + title: 'Delete Entity', + description: 'Delete an entity', + schema: { + input: { type: 'object' as const }, + output: { type: 'object' as const }, + }, + attributes: { destructive: true, readOnly: false, idempotent: false }, + }, + { + id: 'scaffolder:create-app', + pluginId: 'scaffolder', + name: 'create-app', + title: 'Create App', + description: 'Create an app', + schema: { + input: { type: 'object' as const }, + output: { type: 'object' as const }, + }, + attributes: { destructive: false, readOnly: false, idempotent: false }, + }, + ]; + + const fakeActionsService: ActionsService = { + list: jest.fn(async () => ({ actions: fakeActions })), + invoke: jest.fn(async () => ({ output: {} })), + }; + + it('should return all actions when no filter rules are set', async () => { + const mcpService = await McpService.create({ + actions: fakeActionsService, + metrics: metricsServiceMock.mock(), + }); + + const serverConfig: McpServerConfig = { + name: 'All Actions', + includeRules: [], + excludeRules: [], + }; + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(3); + }); + + it('should scope actions using include filter rules', async () => { + const mcpService = await McpService.create({ + actions: fakeActionsService, + metrics: metricsServiceMock.mock(), + }); + + const serverConfig: McpServerConfig = { + name: 'Catalog Only', + includeRules: parseFilterRules( + new ConfigReader({ + include: [{ id: 'catalog:*' }], + }).getConfigArray('include'), + ), + excludeRules: [], + }; + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(2); + expect(result.tools.map(t => t.name)).toEqual([ + 'catalog.get-entity', + 'catalog.delete-entity', + ]); + }); + + it('should apply exclude filter rules to remove destructive actions', async () => { + const mcpService = await McpService.create({ + actions: fakeActionsService, + metrics: metricsServiceMock.mock(), + }); + + const serverConfig: McpServerConfig = { + name: 'Catalog', + includeRules: parseFilterRules( + new ConfigReader({ + include: [{ id: 'catalog:*' }], + }).getConfigArray('include'), + ), + excludeRules: [{ attributes: { destructive: true } }], + }; + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('catalog.get-entity'); + }); + + it('should apply include filter rules with glob patterns', async () => { + const mcpService = await McpService.create({ + actions: fakeActionsService, + metrics: metricsServiceMock.mock(), + }); + + const serverConfig: McpServerConfig = { + name: 'Catalog', + includeRules: parseFilterRules( + new ConfigReader({ + include: [{ id: 'catalog:get-*' }], + }).getConfigArray('include'), + ), + excludeRules: [], + }; + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('catalog.get-entity'); + }); + + it('should reject tool calls for actions outside the filtered set', async () => { + const mcpService = await McpService.create({ + actions: fakeActionsService, + metrics: metricsServiceMock.mock(), + }); + + const serverConfig: McpServerConfig = { + name: 'Scaffolder', + includeRules: parseFilterRules( + new ConfigReader({ + include: [{ id: 'scaffolder:*' }], + }).getConfigArray('include'), + ), + excludeRules: [], + }; + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: 'tools/call', + params: { name: 'catalog.get-entity', arguments: {} }, + }, + CallToolResultSchema, + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: expect.stringContaining( + 'Action "catalog.get-entity" not found', + ), + }, + ], + isError: true, + }); + }); + }); + + describe('server name and description', () => { + it('should default server name to backstage when no config is provided', async () => { + const mcpService = await McpService.create({ + actions: actionsRegistryServiceMock(), + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const serverInfo = client.getServerVersion(); + expect(serverInfo?.name).toBe('backstage'); + expect(serverInfo?.description).toBeUndefined(); + }); + + it('should use name and description from server config', async () => { + const mcpService = await McpService.create({ + actions: actionsRegistryServiceMock(), + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig: { + name: 'My Custom Server', + description: 'A custom MCP server for testing', + includeRules: [], + excludeRules: [], + }, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const serverInfo = client.getServerVersion(); + expect(serverInfo?.name).toBe('My Custom Server'); + expect(serverInfo?.description).toBe('A custom MCP server for testing'); + }); + + it('should omit description when not provided in config', async () => { + const mcpService = await McpService.create({ + actions: actionsRegistryServiceMock(), + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + serverConfig: { + name: 'Named Server', + includeRules: [], + excludeRules: [], + }, + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const serverInfo = client.getServerVersion(); + expect(serverInfo?.name).toBe('Named Server'); + expect(serverInfo?.description).toBeUndefined(); + }); + }); + + describe('namespaced tool names', () => { + it('should use action ID as tool name by default', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'mock-action', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools[0].name).toBe('test.mock-action'); + }); + + it('should use short action name when namespacing is disabled', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'mock-action', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: metricsServiceMock.mock(), + namespacedToolNames: false, + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { method: 'tools/list' }, + ListToolsResultSchema, + ); + + expect(result.tools[0].name).toBe('mock-action'); + }); + + it('should match tool calls using the namespaced name', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'mock-action', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({}), + output: z => z.object({}), + }, + action: async () => ({ output: {} }), + }); + + const mcpService = await McpService.create({ + actions: mockActionsRegistry, + metrics: metricsServiceMock.mock(), + }); + + const server = mcpService.getServer({ + credentials: mockCredentials.user(), + }); + + const client = new Client({ name: 'test', version: '1.0' }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: 'tools/call', + params: { name: 'test.mock-action', arguments: {} }, + }, + CallToolResultSchema, + ); + + expect(result.isError).toBeUndefined(); + }); }); }); diff --git a/plugins/mcp-actions-backend/src/services/McpService.ts b/plugins/mcp-actions-backend/src/services/McpService.ts index c6cdf901d7..49eea11d72 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.ts @@ -20,87 +20,231 @@ import { CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { JsonObject } from '@backstage/types'; -import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { + ActionsService, + ActionsServiceAction, + MetricsServiceHistogram, + MetricsService, +} from '@backstage/backend-plugin-api/alpha'; import { version } from '@backstage/plugin-mcp-actions-backend/package.json'; import { NotFoundError } from '@backstage/errors'; +import { performance } from 'node:perf_hooks'; import { handleErrors } from './handleErrors'; +import { bucketBoundaries, McpServerOperationAttributes } from '../metrics'; +import { FilterRule, McpServerConfig } from '../config'; export class McpService { private readonly actions: ActionsService; + private readonly namespacedToolNames: boolean; + private readonly operationDuration: MetricsServiceHistogram; - constructor(actions: ActionsService) { + constructor( + actions: ActionsService, + metrics: MetricsService, + namespacedToolNames?: boolean, + ) { this.actions = actions; + this.namespacedToolNames = namespacedToolNames ?? true; + this.operationDuration = + metrics.createHistogram( + 'mcp.server.operation.duration', + { + description: 'MCP request duration as observed on the receiver', + unit: 's', + advice: { explicitBucketBoundaries: bucketBoundaries }, + }, + ); } - static async create({ actions }: { actions: ActionsService }) { - return new McpService(actions); + static async create({ + actions, + metrics, + namespacedToolNames, + }: { + actions: ActionsService; + metrics: MetricsService; + namespacedToolNames?: boolean; + }) { + return new McpService(actions, metrics, namespacedToolNames); } - getServer({ credentials }: { credentials: BackstageCredentials }) { + getServer({ + credentials, + serverConfig, + }: { + credentials: BackstageCredentials; + serverConfig?: McpServerConfig; + }) { const server = new McpServer( { - name: 'backstage', + name: serverConfig?.name ?? 'backstage', // TODO: this version will most likely change in the future. version, + ...(serverConfig?.description && { + description: serverConfig.description, + }), }, { capabilities: { tools: {} } }, ); server.setRequestHandler(ListToolsRequestSchema, async () => { - // TODO: switch this to be configuration based later - const { actions } = await this.actions.list({ credentials }); + const startTime = performance.now(); + let errorType: string | undefined; - return { - tools: actions.map(action => ({ - inputSchema: action.schema.input, - // todo(blam): this is unfortunately not supported by most clients yet. - // When this is provided you need to provide structuredContent instead. - // outputSchema: action.schema.output, - name: action.name, - description: action.description, - annotations: { - title: action.title, - destructiveHint: action.attributes.destructive, - idempotentHint: action.attributes.idempotent, - readOnlyHint: action.attributes.readOnly, - openWorldHint: false, - }, - })), - }; + try { + const { actions: allActions } = await this.actions.list({ + credentials, + }); + const actions = serverConfig + ? this.filterActions(allActions, serverConfig) + : allActions; + + return { + tools: actions.map(action => ({ + inputSchema: action.schema.input, + // todo(blam): this is unfortunately not supported by most clients yet. + // When this is provided you need to provide structuredContent instead. + // outputSchema: action.schema.output, + name: this.getToolName(action), + description: action.description, + annotations: { + title: action.title, + destructiveHint: action.attributes.destructive, + idempotentHint: action.attributes.idempotent, + readOnlyHint: action.attributes.readOnly, + openWorldHint: false, + }, + })), + }; + } catch (err) { + errorType = err instanceof Error ? err.name : 'Error'; + throw err; + } finally { + const durationSeconds = (performance.now() - startTime) / 1000; + + this.operationDuration.record(durationSeconds, { + 'mcp.method.name': 'tools/list', + ...(errorType && { 'error.type': errorType }), + }); + } }); server.setRequestHandler(CallToolRequestSchema, async ({ params }) => { - return handleErrors(async () => { - const { actions } = await this.actions.list({ credentials }); - const action = actions.find(a => a.name === params.name); + const startTime = performance.now(); + let errorType: string | undefined; + let isError = false; - if (!action) { - throw new NotFoundError(`Action "${params.name}" not found`); - } + try { + const result = await handleErrors(async () => { + const { actions: allActions } = await this.actions.list({ + credentials, + }); + const actions = serverConfig + ? this.filterActions(allActions, serverConfig) + : allActions; - const { output } = await this.actions.invoke({ - id: action.id, - input: params.arguments as JsonObject, - credentials, + const action = actions.find(a => this.getToolName(a) === params.name); + + if (!action) { + throw new NotFoundError(`Action "${params.name}" not found`); + } + + const { output } = await this.actions.invoke({ + id: action.id, + input: params.arguments as JsonObject, + credentials, + }); + + return { + // todo(blam): unfortunately structuredContent is not supported by most clients yet. + // so the validation for the output happens in the default actions registry + // and we return it as json text instead for now. + content: [ + { + type: 'text', + text: ['```json', JSON.stringify(output, null, 2), '```'].join( + '\n', + ), + }, + ], + }; }); - return { - // todo(blam): unfortunately structuredContent is not supported by most clients yet. - // so the validation for the output happens in the default actions registry - // and we return it as json text instead for now. - content: [ - { - type: 'text', - text: ['```json', JSON.stringify(output, null, 2), '```'].join( - '\n', - ), - }, - ], - }; - }); + isError = !!(result as { isError?: boolean })?.isError; + return result; + } catch (err) { + errorType = err instanceof Error ? err.name : 'Error'; + throw err; + } finally { + const durationSeconds = (performance.now() - startTime) / 1000; + + // Determine error.type per OTel MCP spec: + // - Thrown exceptions use the error name + // - CallToolResult with isError=true uses 'tool_error' + let errorAttribute: string | undefined = errorType; + if (!errorAttribute && isError) { + errorAttribute = 'tool_error'; + } + + this.operationDuration.record(durationSeconds, { + 'mcp.method.name': 'tools/call', + 'gen_ai.tool.name': params.name, + 'gen_ai.operation.name': 'execute_tool', + ...(errorAttribute && { 'error.type': errorAttribute }), + }); + } }); return server; } + + private filterActions( + actions: ActionsServiceAction[], + serverConfig: McpServerConfig, + ): ActionsServiceAction[] { + const { includeRules, excludeRules } = serverConfig; + if (includeRules.length === 0 && excludeRules.length === 0) { + return actions; + } + + return actions.filter(action => { + if (excludeRules.some(rule => this.matchesRule(action, rule))) { + return false; + } + + if (includeRules.length === 0) { + return true; + } + + return includeRules.some(rule => this.matchesRule(action, rule)); + }); + } + + private getToolName(action: ActionsServiceAction): string { + if (this.namespacedToolNames) { + return `${action.pluginId}.${action.name}`; + } + return action.name; + } + + private matchesRule(action: ActionsServiceAction, rule: FilterRule): boolean { + if (rule.idMatcher && !rule.idMatcher.match(action.id)) { + return false; + } + + if (rule.attributes) { + for (const [key, value] of Object.entries(rule.attributes)) { + if ( + action.attributes[ + key as 'destructive' | 'readOnly' | 'idempotent' + ] !== value + ) { + return false; + } + } + } + + return true; + } } diff --git a/plugins/mcp-actions-backend/src/services/handleErrors.test.ts b/plugins/mcp-actions-backend/src/services/handleErrors.test.ts new file mode 100644 index 0000000000..17fe6de0a6 --- /dev/null +++ b/plugins/mcp-actions-backend/src/services/handleErrors.test.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 { + InputError, + NotFoundError, + NotAllowedError, + ForwardedError, + ResponseError, +} from '@backstage/errors'; +import { handleErrors } from './handleErrors'; + +describe('handleErrors', () => { + it('should return the error description for a known error', async () => { + const result = await handleErrors(async () => { + throw new InputError('bad input'); + }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'InputError: bad input' }], + isError: true, + }); + }); + + it('should return the error description for a NotFoundError', async () => { + const result = await handleErrors(async () => { + throw new NotFoundError('entity not found'); + }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'NotFoundError: entity not found' }], + isError: true, + }); + }); + + it('should return the error description for a NotAllowedError', async () => { + const result = await handleErrors(async () => { + throw new NotAllowedError('forbidden'); + }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'NotAllowedError: forbidden' }], + isError: true, + }); + }); + + it('should rethrow an unknown error', async () => { + await expect( + handleErrors(async () => { + throw new Error('unknown problem'); + }), + ).rejects.toThrow('unknown problem'); + }); + + it('should handle a ForwardedError that inherits the cause name', async () => { + const result = await handleErrors(async () => { + throw new ForwardedError( + 'wrapper message', + new InputError('original error'), + ); + }); + + // ForwardedError inherits name from cause and CustomErrorBase + // concatenates the cause message into the full message + expect(result).toEqual({ + content: [ + { + type: 'text', + text: 'InputError: wrapper message; caused by InputError: original error', + }, + ], + isError: true, + }); + }); + + it('should extract the cause from a ResponseError', async () => { + const response = { + ok: false, + status: 400, + statusText: 'Bad Request', + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => + JSON.stringify({ + error: { name: 'InputError', message: 'bad value' }, + response: { statusCode: 400 }, + }), + }; + + const responseError = await ResponseError.fromResponse(response as any); + + const result = await handleErrors(async () => { + throw responseError; + }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'InputError: bad value' }], + isError: true, + }); + }); + + it('should recursively extract through nested ResponseErrors', async () => { + const innerResponse = { + ok: false, + status: 400, + statusText: 'Bad Request', + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => + JSON.stringify({ + error: { + name: 'ResponseError', + message: 'Request failed with 400 Bad Request', + cause: { name: 'InputError', message: 'deeply nested error' }, + }, + response: { statusCode: 400 }, + }), + }; + + const responseError = await ResponseError.fromResponse( + innerResponse as any, + ); + + const result = await handleErrors(async () => { + throw responseError; + }); + + expect(result).toEqual({ + content: [{ type: 'text', text: 'InputError: deeply nested error' }], + isError: true, + }); + }); +}); diff --git a/plugins/mcp-actions-backend/src/services/handleErrors.ts b/plugins/mcp-actions-backend/src/services/handleErrors.ts index 5266c80e75..97070bb723 100644 --- a/plugins/mcp-actions-backend/src/services/handleErrors.ts +++ b/plugins/mcp-actions-backend/src/services/handleErrors.ts @@ -35,14 +35,14 @@ const knownErrors = new Set([ 'ServiceUnavailableError', ]); -// Extracts the cause error, if the provided error is `ResponseError` or -// `ForwardedError` with a cause. +// Recursively extracts the innermost cause from ResponseError or +// ForwardedError wrappers to surface the original error. function extractCause(err: ErrorLike): ErrorLike { if ( (err.name === 'ResponseError' || err instanceof ForwardedError) && isError(err.cause) ) { - return err.cause; + return extractCause(err.cause); } return err; } diff --git a/plugins/mui-to-bui/CHANGELOG.md b/plugins/mui-to-bui/CHANGELOG.md index 2c7a57d3ef..16348d06bf 100644 --- a/plugins/mui-to-bui/CHANGELOG.md +++ b/plugins/mui-to-bui/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-mui-to-bui +## 0.2.5-next.2 + +### Patch Changes + +- ad7c883: Updated the MUI to BUI theme converter page to use the renamed `Header` component from `@backstage/ui`. +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + +## 0.2.4 + +### Patch Changes + +- 4137a43: Updated CSS token references to use renamed `--bui-bg-app` and `--bui-border-2` tokens. +- a88c437: Updated MUI to BUI theme converter to align with latest token changes + + **Changes:** + + - Removed generation of deprecated tokens: `--bui-fg-link`, `--bui-fg-link-hover`, `--bui-fg-tint`, `--bui-fg-tint-disabled`, `--bui-bg-tint` and all its variants + - Added generation of new `info` status tokens: `--bui-fg-info`, `--bui-fg-info-on-bg`, `--bui-bg-info`, `--bui-border-info` + - Updated status color mapping to generate both standalone and `-on-bg` variants for danger, warning, success, and info + - Status colors now use `.main` for standalone variants and `.dark` for `-on-bg` variants, providing better visual hierarchy + + The converter now generates tokens that match the updated BUI design system structure, with clear distinction between status colors for standalone use vs. use on colored backgrounds. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/ui@0.12.0 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.2.4-next.2 ### Patch Changes diff --git a/plugins/mui-to-bui/README.md b/plugins/mui-to-bui/README.md index d964b4827f..0b760c4ef2 100644 --- a/plugins/mui-to-bui/README.md +++ b/plugins/mui-to-bui/README.md @@ -6,17 +6,24 @@ The Backstage UI Themer helps you convert an existing MUI v5 theme into Backstag ## Installation -### 1) Add the dependency to your app - -Run this from your Backstage repo root: +Add the dependency to your app: ```bash yarn --cwd packages/app add @backstage/plugin-mui-to-bui ``` -### 2) Wire it up depending on your frontend system +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). -#### Old frontend system (legacy `App.tsx` with ``) +## Accessing the Themer page + +- Navigate to `/mui-to-bui` in your Backstage app (for example `http://localhost:3000/mui-to-bui`). +- Optional: Add a sidebar/link in your app that points to `/mui-to-bui` if you want a permanent navigation entry. + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the +plugin into your app as outlined in this section. If you are on the new frontend +system, you can skip this. Add a route for the page in your app: @@ -34,30 +41,3 @@ export const App = () => ( ); ``` - -#### New frontend system - -If package discovery is enabled in your app, this plugin is picked up automatically after installation — no code changes required. Just navigate to `/mui-to-bui`. - -If you prefer explicit registration (or don't use discovery), register the plugin as a feature. The page route (`/mui-to-bui`) is provided by the plugin. - -```tsx -// packages/app/src/App.tsx (or your app entry where you call createApp) -import React from 'react'; -import { createApp } from '@backstage/frontend-defaults'; -import buiThemerPlugin from '@backstage/plugin-mui-to-bui'; - -const app = createApp({ - features: [ - // ...other features - buiThemerPlugin, - ], -}); - -export default app.createRoot(); -``` - -## Accessing the Themer page - -- Navigate to `/mui-to-bui` in your Backstage app (for example `http://localhost:3000/mui-to-bui`). -- Optional: Add a sidebar/link in your app that points to `/mui-to-bui` if you want a permanent navigation entry. diff --git a/plugins/mui-to-bui/package.json b/plugins/mui-to-bui/package.json index de43742c20..a954b1028c 100644 --- a/plugins/mui-to-bui/package.json +++ b/plugins/mui-to-bui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mui-to-bui", - "version": "0.2.4-next.2", + "version": "0.2.5-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "mui-to-bui", diff --git a/plugins/mui-to-bui/report.api.md b/plugins/mui-to-bui/report.api.md index db1d49f828..f4bf566c9e 100644 --- a/plugins/mui-to-bui/report.api.md +++ b/plugins/mui-to-bui/report.api.md @@ -5,7 +5,10 @@ ```ts import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { JSX as JSX_3 } from 'react/jsx-runtime'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -36,26 +39,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx index 7f0e9f32fc..a9b2c07318 100644 --- a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemerPage.tsx @@ -16,7 +16,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { appThemeApiRef } from '@backstage/core-plugin-api'; -import { Box, Card, Container, Flex, HeaderPage, Text } from '@backstage/ui'; +import { Box, Card, Container, Flex, Header, Text } from '@backstage/ui'; import { ThemeContent } from './ThemeContent'; import { MuiThemeExtractor } from './MuiThemeExtractor'; @@ -26,7 +26,7 @@ export const BuiThemerPage = () => { return ( - +
    Convert your MUI v5 theme into BUI CSS variables. Pick a theme to view diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 32fdc6839e..d25fd32fea 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + +## 0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + +## 0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## 0.3.18 + +### Patch Changes + +- e9eb400: Allow configuring included topics for email notifications. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index fd6a957072..0b1eb9fde4 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.18-next.1", + "version": "0.3.19-next.2", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index 2456a92cea..b08ace896f 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.4.0-next.1 + +### Minor Changes + +- cd62d78: **BREAKING**: Only send direct messages to user entity recipients. Notifications sent to non-user entities no longer send Slack direct messages to resolved users. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + +## 0.4.0-next.0 + +### Minor Changes + +- 749ba60: Add an extension for custom Slack message layouts + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + ## 0.3.1-next.1 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 7d4a9f244b..7cc7b280ca 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.3.1-next.1", + "version": "0.4.0-next.1", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/report.api.md b/plugins/notifications-backend-module-slack/report.api.md index 3c4ddb494d..6fb477c1ee 100644 --- a/plugins/notifications-backend-module-slack/report.api.md +++ b/plugins/notifications-backend-module-slack/report.api.md @@ -4,6 +4,9 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { KnownBlock } from '@slack/web-api'; +import { NotificationPayload } from '@backstage/plugin-notifications-common'; // @public export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify'; @@ -11,4 +14,18 @@ export const ANNOTATION_SLACK_BOT_NOTIFY = 'slack.com/bot-notify'; // @public const notificationsModuleSlack: BackendFeature; export default notificationsModuleSlack; + +// @public +export interface NotificationsSlackBlockKitExtensionPoint { + // (undocumented) + setBlockKitRenderer(renderer: SlackBlockKitRenderer): void; +} + +// @public (undocumented) +export const notificationsSlackBlockKitExtensionPoint: ExtensionPoint; + +// @public (undocumented) +export type SlackBlockKitRenderer = ( + payload: NotificationPayload, +) => KnownBlock[]; ``` diff --git a/plugins/notifications-backend-module-slack/src/extensions.ts b/plugins/notifications-backend-module-slack/src/extensions.ts new file mode 100644 index 0000000000..c2465b69cf --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/extensions.ts @@ -0,0 +1,43 @@ +/* + * 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 { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { NotificationPayload } from '@backstage/plugin-notifications-common'; +import { KnownBlock } from '@slack/web-api'; + +/** + * @public + */ +export type SlackBlockKitRenderer = ( + payload: NotificationPayload, +) => KnownBlock[]; + +/** + * @public + * + * Extension point for customizing how notification payloads are rendered into + * Slack Block Kit messages before they're sent. + */ +export interface NotificationsSlackBlockKitExtensionPoint { + setBlockKitRenderer(renderer: SlackBlockKitRenderer): void; +} + +/** + * @public + */ +export const notificationsSlackBlockKitExtensionPoint = + createExtensionPoint({ + id: 'notifications.slack.blockkit', + }); diff --git a/plugins/notifications-backend-module-slack/src/index.ts b/plugins/notifications-backend-module-slack/src/index.ts index f8a9e248f7..178c5dbc54 100644 --- a/plugins/notifications-backend-module-slack/src/index.ts +++ b/plugins/notifications-backend-module-slack/src/index.ts @@ -22,3 +22,4 @@ export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib'; export { notificationsModuleSlack as default } from './module'; +export * from './extensions'; 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 9490ff7dde..ce7898566b 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -17,7 +17,7 @@ import { mockServices } from '@backstage/backend-test-utils'; import { SlackNotificationProcessor } from './SlackNotificationProcessor'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; -import { WebClient } from '@slack/web-api'; +import { KnownBlock, WebClient } from '@slack/web-api'; import { Entity } from '@backstage/catalog-model'; import pThrottle from 'p-throttle'; import { durationToMilliseconds } from '@backstage/types'; @@ -209,6 +209,43 @@ describe('SlackNotificationProcessor', () => { }); }); + it('should use a custom block kit renderer when provided', async () => { + const slack = new WebClient(); + const customBlocks: KnownBlock[] = [ + { + type: 'section', + text: { type: 'mrkdwn', text: 'Custom block' }, + }, + ]; + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + blockKitRenderer: () => customBlocks, + })[0]; + + await processor.processOptions({ + recipients: { type: 'entity', entityRef: 'group:default/mock' }, + payload: { title: 'notification' }, + }); + + expect(slack.chat.postMessage).toHaveBeenCalledWith({ + channel: 'C12345678', + text: 'notification', + attachments: [ + { + color: '#00A699', + blocks: customBlocks, + fallback: 'notification', + }, + ], + }); + }); + describe('when a user notification is sent directly', () => { it('should send a notification to a user', async () => { const slack = new WebClient(); @@ -318,6 +355,51 @@ describe('SlackNotificationProcessor', () => { }); }); + describe('when recipients include both users and a group', () => { + it('should still DM explicit user recipients', async () => { + const slack = new WebClient(); + + const processor = SlackNotificationProcessor.fromConfig(config, { + auth, + logger, + catalog: catalogServiceMock({ + entities: DEFAULT_ENTITIES_RESPONSE.items, + }), + slack, + })[0]; + + await processor.processOptions({ + recipients: { + type: 'entity', + entityRef: ['group:default/mock', 'user:default/mock'], + }, + payload: { title: 'notification' }, + }); + + await processor.postProcess( + { + origin: 'plugin', + id: 'explicit-user-1', + user: 'user:default/mock', + created: new Date(), + payload: { title: 'notification' }, + }, + { + recipients: { + type: 'entity', + entityRef: ['group:default/mock', 'user:default/mock'], + }, + payload: { title: 'notification' }, + }, + ); + + expect(slack.chat.postMessage).toHaveBeenCalledTimes(2); + expect(slack.chat.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ channel: 'U12345678' }), + ); + }); + }); + describe('when broadcast channels are not configured', () => { it('should not send broadcast messages', async () => { const slack = new WebClient(); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index e8f8d9eda4..de084df464 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -19,6 +19,7 @@ import { Entity, isUserEntity, parseEntityRef, + stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; import { Config, readDurationFromConfig } from '@backstage/config'; @@ -37,6 +38,7 @@ import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants'; import { BroadcastRoute } from './types'; import { ExpiryMap, toChatPostMessageArgs } from './util'; import { CatalogService } from '@backstage/plugin-catalog-node'; +import { SlackBlockKitRenderer } from '../extensions'; export class SlackNotificationProcessor implements NotificationProcessor { private readonly logger: LoggerService; @@ -54,6 +56,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly username?: string; private readonly concurrencyLimit: number; private readonly throttleInterval: number; + private readonly blockKitRenderer?: SlackBlockKitRenderer; static fromConfig( config: Config, @@ -63,6 +66,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { catalog: CatalogService; slack?: WebClient; broadcastChannels?: string[]; + blockKitRenderer?: SlackBlockKitRenderer; }, ): SlackNotificationProcessor[] { const slackConfig = @@ -104,6 +108,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { username?: string; concurrencyLimit?: number; throttleInterval?: number; + blockKitRenderer?: SlackBlockKitRenderer; }) { const { auth, @@ -115,6 +120,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { username, concurrencyLimit, throttleInterval, + blockKitRenderer, } = options; this.logger = logger; this.catalog = catalog; @@ -126,6 +132,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { this.concurrencyLimit = concurrencyLimit ?? 10; this.throttleInterval = throttleInterval ?? durationToMilliseconds({ minutes: 1 }); + this.blockKitRenderer = blockKitRenderer; this.entityLoader = new DataLoader( async entityRefs => { @@ -246,6 +253,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { channel, payload: options.payload, username: this.username, + blockKitRenderer: this.blockKitRenderer, }); this.logger.debug( @@ -273,8 +281,15 @@ export class SlackNotificationProcessor implements NotificationProcessor { } else if (options.recipients.type === 'entity') { // Handle user-specific notification const entityRefs = [options.recipients.entityRef].flat(); - if (entityRefs.some(e => parseEntityRef(e).kind === 'group')) { - // We've already dispatched a slack channel message, so let's not send a DM. + const explicitUserEntityRefs = entityRefs + .filter(entityRef => parseEntityRef(entityRef).kind === 'user') + .map(entityRef => stringifyEntityRef(parseEntityRef(entityRef))); + const normalizedUserRef = stringifyEntityRef( + parseEntityRef(notification.user), + ); + + if (!explicitUserEntityRefs.includes(normalizedUserRef)) { + // This user was resolved from a non-user entity. Skip sending a DM. return; } @@ -306,6 +321,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { channel, payload: formattedPayload, username: this.username, + blockKitRenderer: this.blockKitRenderer, }), ); diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts index 33293ea24f..0c13b21be6 100644 --- a/plugins/notifications-backend-module-slack/src/lib/util.ts +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -19,13 +19,16 @@ import { NotificationSeverity, } from '@backstage/plugin-notifications-common'; import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api'; +import { SlackBlockKitRenderer } from '../extensions'; export function toChatPostMessageArgs(options: { channel: string; payload: NotificationPayload; username?: string; + blockKitRenderer?: SlackBlockKitRenderer; }): ChatPostMessageArguments { - const { channel, payload, username } = options; + const { channel, payload, username, blockKitRenderer } = options; + const blocks = (blockKitRenderer ?? toSlackBlockKit)(payload); const args: ChatPostMessageArguments = { channel, @@ -34,7 +37,7 @@ export function toChatPostMessageArgs(options: { attachments: [ { color: getColor(payload.severity), - blocks: toSlackBlockKit(payload), + blocks, fallback: payload.title, }, ], diff --git a/plugins/notifications-backend-module-slack/src/module.ts b/plugins/notifications-backend-module-slack/src/module.ts index ff783ef104..e3d6aebee5 100644 --- a/plugins/notifications-backend-module-slack/src/module.ts +++ b/plugins/notifications-backend-module-slack/src/module.ts @@ -20,6 +20,10 @@ import { import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { + notificationsSlackBlockKitExtensionPoint, + SlackBlockKitRenderer, +} from './extensions'; /** * The Slack notification processor for use with the notifications plugin. @@ -31,6 +35,16 @@ export const notificationsModuleSlack = createBackendModule({ pluginId: 'notifications', moduleId: 'slack', register(reg) { + let blockKitRenderer: SlackBlockKitRenderer | undefined; + reg.registerExtensionPoint(notificationsSlackBlockKitExtensionPoint, { + setBlockKitRenderer(renderer) { + if (blockKitRenderer) { + throw new Error(`Slack block kit renderer was already registered`); + } + blockKitRenderer = renderer; + }, + }); + reg.registerInit({ deps: { auth: coreServices.auth, @@ -45,6 +59,7 @@ export const notificationsModuleSlack = createBackendModule({ auth, logger, catalog, + blockKitRenderer, }), ); }, diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 8cd9046777..4ddfb75c33 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-notifications-backend +## 0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-notifications-node@0.2.24-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## 0.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## 0.6.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- e9eb400: Allow configuring included topics for email notifications. +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + - @backstage/plugin-signals-node@0.1.28 + ## 0.6.2-next.2 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 7ccc715589..ee6b615bdb 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.6.2-next.2", + "version": "0.6.3-next.1", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-common/CHANGELOG.md b/plugins/notifications-common/CHANGELOG.md index 623ced5b1d..5bb8925474 100644 --- a/plugins/notifications-common/CHANGELOG.md +++ b/plugins/notifications-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-notifications-common +## 0.2.1 + +### Patch Changes + +- e9eb400: Allow configuring included topics for email notifications. + ## 0.2.0 ### Minor Changes diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 1fad4544c7..6163256843 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-common", - "version": "0.2.0", + "version": "0.2.1", "description": "Common functionalities for the notifications plugin", "backstage": { "role": "common-library", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 92e7469332..2393e048ce 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-notifications-node +## 0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## 0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## 0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.28 + ## 0.2.23-next.1 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index bc3871dd6f..3b847d8b37 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.23-next.1", + "version": "0.2.24-next.2", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index c8bb983163..e8a90da0f1 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-notifications +## 0.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## 0.5.14 + +### Patch Changes + +- 8005286: Added `renderItem` prop to `NotificationsSidebarItem` component, allowing custom UI rendering while retaining all built-in notification logic (unread count, snackbar, signals, web notifications). +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-signals-react@0.0.19 + ## 0.5.14-next.2 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 2d6a84fc13..e550a8d75e 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.14-next.2", + "version": "0.5.15-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/report-alpha.api.md b/plugins/notifications/report-alpha.api.md index 9a27b67d80..e1feed0cde 100644 --- a/plugins/notifications/report-alpha.api.md +++ b/plugins/notifications/report-alpha.api.md @@ -6,8 +6,11 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -42,26 +45,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; } diff --git a/plugins/notifications/report.api.md b/plugins/notifications/report.api.md index 8a1fd47bad..2c056d4e91 100644 --- a/plugins/notifications/report.api.md +++ b/plugins/notifications/report.api.md @@ -169,20 +169,9 @@ export type NotificationsSideBarItemProps = { }; // @public (undocumented) -export const NotificationsTable: ({ - title, - markAsReadOnLinkOpen, - isLoading, - notifications, - isUnread, - onUpdate, - setContainsText, - onPageChange, - onRowsPerPageChange, - page, - pageSize, - totalCount, -}: NotificationsTableProps) => JSX_2.Element; +export const NotificationsTable: ( + input: NotificationsTableProps, +) => JSX_2.Element; // @public (undocumented) export type NotificationsTableProps = Pick< diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index c028c7c9a9..1fc4b58097 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-org-react +## 0.1.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.1.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 0.1.47 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + ## 0.1.47-next.2 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 05ac59496e..191a63b696 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.47-next.2", + "version": "0.1.48-next.1", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index ad04247193..ecea7ba558 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,96 @@ # @backstage/plugin-org +## 0.7.0-next.2 + +### Minor Changes + +- d14b6e0: **BREAKING**: Migrated `MembersListCard`, `OwnershipCard`, and `CatalogGraphCard` to use BUI card primitives via `EntityInfoCard`. + + - `OwnershipCard`: Removed `variant` and `maxScrollHeight` props. Card height and scrolling are now controlled by the parent container — the card fills its container and the body scrolls automatically when content overflows. + - `CatalogGraphCard`: Removed `variant` prop. + - `MembersListCard`: Translation keys `subtitle`, `paginationLabel`, `aggregateMembersToggle.directMembers`, `aggregateMembersToggle.aggregatedMembers`, and `aggregateMembersToggle.ariaLabel` have been removed. The `title` key now includes `{{groupName}}`. New keys added: `cardLabel`, `noSearchResult`, `aggregateMembersToggle.label`. + - `OwnershipCard`: Translation keys `aggregateRelationsToggle.directRelations`, `aggregateRelationsToggle.aggregatedRelations`, and `aggregateRelationsToggle.ariaLabel` have been removed. New key added: `aggregateRelationsToggle.label`. + - Removed `MemberComponentClassKey` export, and `root` and `cardContent` from `MembersListCardClassKey`, `card` from `OwnershipCardClassKey`, and `card` from `CatalogGraphCardClassKey`. + + **Migration:** + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +- 5fc35bb: Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + + **BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + + **Migration:** + + Simply delete the obsolete `variant` and `gridSizes` props, e.g: + + ```diff + - + + + ``` + + ```diff + - + + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 0.6.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.6.50-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## 0.6.49 + +### Patch Changes + +- ac9bead: Added `@backstage/frontend-test-utils` dev dependency. +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 4183614: Updated usage of deprecated APIs in the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 1dee6de: Add search functionality in MembersListCard +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.6.49-next.2 ### Patch Changes diff --git a/plugins/org/README-alpha.md b/plugins/org/README-alpha.md index 28e274b58c..db8a84f35c 100644 --- a/plugins/org/README-alpha.md +++ b/plugins/org/README-alpha.md @@ -1,8 +1,6 @@ -# Org Plugin +# Org Plugin - Extension Reference -> [!WARNING] -> This documentation is made for those using the experimental new Frontend system. -> If you are not using the new frontend system, please go [here](./README.md). +This page contains detailed documentation for all extensions provided by the `@backstage/plugin-org` plugin. For general information about the plugin, see the [README](./README.md). This is a plugin that extends the Catalog entity page with some users and groups overview cards: diff --git a/plugins/org/README.md b/plugins/org/README.md index 9cf165f06d..062f88fa98 100644 --- a/plugins/org/README.md +++ b/plugins/org/README.md @@ -1,8 +1,5 @@ # Org Plugin for Backstage -> Disclaimer: -> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md). - ## Features - Show Group Page @@ -21,6 +18,33 @@ Here's an example of what the User Profile looks like: ![Group Page example](./docs/user-profile-example.png) +## Installation + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-org +``` + +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). + +You can enable entity cards on the catalog entity page through configuration: + +```yaml +# app-config.yaml +app: + extensions: + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile +``` + +For the full list of available extensions and their configuration options, see the [README-alpha.md](./README-alpha.md). + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + ### MyGroupsSidebarItem The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar. diff --git a/plugins/org/package.json b/plugins/org/package.json index ef68d4e71a..88da2205d9 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.49-next.2", + "version": "0.7.0-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", @@ -57,6 +57,7 @@ "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/ui": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index 781cfe605a..44fd429bfa 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -214,17 +214,13 @@ export const orgTranslationRef: TranslationRef< readonly 'groupProfileCard.listItemTitle.entityRef': 'Entity Ref'; readonly 'groupProfileCard.listItemTitle.parentGroup': 'Parent Group'; readonly 'groupProfileCard.listItemTitle.childGroups': 'Child Groups'; - readonly 'membersListCard.title': 'Members'; - readonly 'membersListCard.subtitle': 'of {{groupName}}'; - readonly 'membersListCard.paginationLabel': ', page {{page}} of {{nbPages}}'; + readonly 'membersListCard.title': '{{groupName}} members'; + readonly 'membersListCard.cardLabel': 'User page for {{memberName}}'; readonly 'membersListCard.noMembersDescription': 'This group has no members.'; - readonly 'membersListCard.aggregateMembersToggle.ariaLabel': 'Users Type Switch'; - readonly 'membersListCard.aggregateMembersToggle.directMembers': 'Direct Members'; - readonly 'membersListCard.aggregateMembersToggle.aggregatedMembers': 'Aggregated Members'; + readonly 'membersListCard.noSearchResult': 'Found no members matching "{{searchTerm}}".'; + readonly 'membersListCard.aggregateMembersToggle.label': 'Include subgroups'; readonly 'ownershipCard.title': 'Ownership'; - readonly 'ownershipCard.aggregateRelationsToggle.ariaLabel': 'Ownership Type Switch'; - readonly 'ownershipCard.aggregateRelationsToggle.directRelations': 'Direct Relations'; - readonly 'ownershipCard.aggregateRelationsToggle.aggregatedRelations': 'Aggregated Relations'; + readonly 'ownershipCard.aggregateRelationsToggle.label': 'Include indirect ownership'; readonly 'userProfileCard.editIconButtonTitle': 'Edit Metadata'; readonly 'userProfileCard.listItemTitle.email': 'Email'; readonly 'userProfileCard.listItemTitle.memberOf': 'Member of'; diff --git a/plugins/org/report.api.md b/plugins/org/report.api.md index 13143d0e4c..d51a45e3f7 100644 --- a/plugins/org/report.api.md +++ b/plugins/org/report.api.md @@ -6,7 +6,6 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; -import { InfoCardVariants } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react/jsx-runtime'; // @public (undocumented) @@ -18,7 +17,6 @@ export type ComponentsGridClassKey = // @public (undocumented) export const EntityGroupProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; }) => JSX_2.Element; @@ -34,13 +32,11 @@ export const EntityMembersListCard: (props: { // @public (undocumented) export const EntityOwnershipCard: (props: { - variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; relationsType?: EntityRelationAggregation; relationAggregation?: EntityRelationAggregation; entityLimit?: number; - maxScrollHeight?: string; }) => JSX_2.Element; // @public (undocumented) @@ -48,7 +44,6 @@ export type EntityRelationAggregation = 'direct' | 'aggregated'; // @public (undocumented) export const EntityUserProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; @@ -56,13 +51,9 @@ export const EntityUserProfileCard: (props: { // @public (undocumented) export const GroupProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; }) => JSX_2.Element; -// @public (undocumented) -export type MemberComponentClassKey = 'card' | 'avatar'; - // @public (undocumented) export const MembersListCard: (props: { memberDisplayTitle?: string; @@ -74,7 +65,7 @@ export const MembersListCard: (props: { }) => JSX_2.Element; // @public (undocumented) -export type MembersListCardClassKey = 'root' | 'cardContent' | 'memberList'; +export type MembersListCardClassKey = 'memberList'; // @public export const MyGroupsSidebarItem: (props: { @@ -96,27 +87,18 @@ export { orgPlugin as plugin }; // @public (undocumented) export const OwnershipCard: (props: { - variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; relationsType?: EntityRelationAggregation; relationAggregation?: EntityRelationAggregation; entityLimit?: number; - maxScrollHeight?: string; }) => JSX_2.Element; // @public (undocumented) -export type OwnershipCardClassKey = - | 'card' - | 'cardContent' - | 'list' - | 'listItemText' - | 'listItemSecondaryAction' - | 'grid'; +export type OwnershipCardClassKey = 'grid'; // @public (undocumented) export const UserProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 89eba12b97..5810a0447d 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -22,13 +22,7 @@ import { RELATION_PARENT_OF, stringifyEntityRef, } from '@backstage/catalog-model'; -import { - Avatar, - InfoCard, - InfoCardVariants, - Link, -} from '@backstage/core-components'; -import Box from '@material-ui/core/Box'; +import { Link } from '@backstage/core-components'; import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -36,6 +30,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Tooltip from '@material-ui/core/Tooltip'; import { + EntityInfoCard, EntityRefLinks, catalogApiRef, getEntityRelations, @@ -57,6 +52,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { orgTranslationRef } from '../../../../translation'; import { makeStyles } from '@material-ui/core/styles'; +import { Avatar, Flex, Box, Text } from '@backstage/ui'; const useStyles = makeStyles(theme => ({ container: { @@ -71,18 +67,21 @@ const useStyles = makeStyles(theme => ({ }, })); -const CardTitle = (props: { title: string }) => ( - - - {props.title} - +const CardTitle = (props: { title: string; pictureSrc?: string }) => ( + + + {props.title} + ); /** @public */ -export const GroupProfileCard = (props: { - variant?: InfoCardVariants; - showLinks?: boolean; -}) => { +export const GroupProfileCard = (props: { showLinks?: boolean }) => { + const { showLinks } = props; const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); const { entity: group } = useEntity(); @@ -148,11 +147,9 @@ export const GroupProfileCard = (props: { ); return ( - } - subheader={description} - variant={props.variant} - action={ + } + headerActions={ <> {allowRefresh && canRefresh && ( } > - - + {description && {description}} + @@ -234,9 +231,9 @@ export const GroupProfileCard = (props: { secondary={t('groupProfileCard.listItemTitle.childGroups')} /> - {props?.showLinks && } + {showLinks && } - + ); }; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 891c1ba3c7..5ae60fb9b2 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -124,19 +124,11 @@ describe('MemberTab Test', () => { }, }); - expect(screen.getByAltText('Tara MacGovern')).toHaveAttribute( - 'src', - 'https://example.com/staff/tara.jpeg', - ); + expect(screen.getByText('Tara MacGovern')).toBeInTheDocument(); expect(screen.getByText('tara-macgovern@example.com')).toBeInTheDocument(); - expect(screen.getByText('Tara MacGovern').closest('a')).toHaveAttribute( - 'href', - '/catalog/foo-bar/user/tara.macgovern', - ); - expect(screen.getByText('Super Awesome Developer')).toBeInTheDocument(); - expect(screen.getByText('Members (1 of 1)')).toBeInTheDocument(); + expect(screen.getByText('team-d members (1 of 1)')).toBeInTheDocument(); }); it('Can render different member display title', async () => { @@ -205,8 +197,7 @@ describe('MemberTab Test', () => { }, }, ); - const toggleSwitch = screen.queryByRole('checkbox'); - expect(toggleSwitch).toBeNull(); + expect(screen.queryByRole('switch')).not.toBeInTheDocument(); }); it('Shows the aggregate members toggle if the showAggregateMembersToggle prop is true', async () => { @@ -233,8 +224,9 @@ describe('MemberTab Test', () => { }, }, ); - expect(screen.queryByRole('checkbox')).toBeInTheDocument(); + expect(screen.getByRole('switch')).toBeInTheDocument(); }); + it('Shows only direct members if the showAggregateMembersToggle prop is undefined', async () => { await renderInTestApp( { }, }, ); - const displayedMemberNames = screen.queryAllByTestId('user-link'); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); - expect(displayedMemberNames).toHaveLength(2); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(2); expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); expect( @@ -294,10 +285,9 @@ describe('MemberTab Test', () => { }, }, ); - const displayedMemberNames = screen.queryAllByTestId('user-link'); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); - expect(displayedMemberNames).toHaveLength(2); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(2); expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); expect( @@ -331,18 +321,16 @@ describe('MemberTab Test', () => { ); // Should show only direct users on initial load - const displayedMemberNamesBefore = screen.queryAllByTestId('user-link'); - expect(displayedMemberNamesBefore).toHaveLength(2); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(2); // Click the toggle switch - await userEvent.click(screen.getByRole('checkbox')); - const displayedMemberNamesAfter = screen.queryAllByTestId('user-link'); + await userEvent.click(screen.getByRole('switch')); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); const groupBUserOneText = screen.getByText('Group B User One'); const groupDUserOneText = screen.getByText('Group D User One'); const groupEUserOneText = screen.getByText('Group E User One'); - expect(displayedMemberNamesAfter).toHaveLength(5); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(5); expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); expect(groupBUserOneText).toBeInTheDocument(); @@ -392,15 +380,13 @@ describe('MemberTab Test', () => { ); // Should show aggregated users on initial load - const displayedMemberNamesBefore = screen.queryAllByTestId('user-link'); - expect(displayedMemberNamesBefore).toHaveLength(5); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(5); // Click the toggle switch - await userEvent.click(screen.getByRole('checkbox')); + await userEvent.click(screen.getByRole('switch')); // Should now show only direct users - const displayedMemberNamesAfter = screen.queryAllByTestId('user-link'); - expect(displayedMemberNamesAfter).toHaveLength(2); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(2); }); it('Can show aggregated members without the aggregate members toggle', async () => { @@ -428,12 +414,11 @@ describe('MemberTab Test', () => { }, ); - // aggregated relations checkbox should not be rendered - expect(screen.queryByRole('checkbox')).toBeNull(); + // aggregated relations switch should not be rendered + expect(screen.queryByRole('switch')).not.toBeInTheDocument(); // Should show all descendant users on load - const displayedMemberNames = screen.queryAllByTestId('user-link'); - expect(displayedMemberNames).toHaveLength(5); + expect(screen.getAllByRole('heading', { level: 4 })).toHaveLength(5); }); describe('Search', () => { @@ -483,7 +468,7 @@ describe('MemberTab Test', () => { ); expect( - screen.getByText(/This group has no members./i), + screen.getByText(/Found no members matching/i), ).toBeInTheDocument(); }); }); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index ee2f37ea9c..feb8590826 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -23,25 +23,14 @@ import { import { catalogApiRef, useEntity, - EntityRefLink, + EntityInfoCard, + useEntityRefLink, } from '@backstage/plugin-catalog-react'; -import Box from '@material-ui/core/Box'; -import Grid from '@material-ui/core/Grid'; -import Switch from '@material-ui/core/Switch'; -import Typography from '@material-ui/core/Typography'; -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; -import Pagination from '@material-ui/lab/Pagination'; -import { useState, useEffect, ChangeEvent } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useState, useEffect } from 'react'; import useAsync from 'react-use/esm/useAsync'; -import { - Avatar, - InfoCard, - Progress, - ResponseErrorPanel, - Link, - OverflowTooltip, -} from '@backstage/core-components'; +import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { getAllDesendantMembersForGroupEntity, @@ -50,101 +39,107 @@ import { import { EntityRelationAggregation } from '../../types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { orgTranslationRef } from '../../../../translation'; -import TextField from '@material-ui/core/TextField'; +import { + Avatar, + Box, + Card, + Flex, + Link, + SearchField, + Switch, + TablePagination, + Text, +} from '@backstage/ui'; -/** @public */ -export type MemberComponentClassKey = 'card' | 'avatar'; - -const useStyles = makeStyles( - (theme: Theme) => - createStyles({ - card: { - border: `1px solid ${theme.palette.divider}`, - boxShadow: theme.shadows[2], - borderRadius: '4px', - overflow: 'visible', - position: 'relative', - margin: theme.spacing(4, 1, 1), - flex: '1', - minWidth: '0px', - }, - avatar: { - position: 'absolute', - top: '-2rem', - }, - }), - { name: 'PluginOrgMemberComponent' }, -); +const useMemberStyles = makeStyles({ + card: { + display: 'flex', + gap: 'var(--bui-space-3)', + padding: 'var(--bui-space-3)', + alignItems: 'flex-start', + flexDirection: 'row', + height: 140, + overflow: 'hidden', + }, + avatar: { + flexShrink: 0, + }, + cardTextContainer: { + overflow: 'hidden', + }, + singlelineEllipsis: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + multilineEllipsis: { + display: '-webkit-box', + '-webkit-line-clamp': '3', + '-webkit-box-orient': 'vertical', + overflow: 'hidden', + }, +}); const MemberComponent = (props: { member: UserEntity }) => { - const classes = useStyles(); + const { t } = useTranslationRef(orgTranslationRef); + const classes = useMemberStyles(); const { metadata: { name: metaName, description }, spec: { profile }, } = props.member; const displayName = profile?.displayName ?? metaName; + const entityLink = useEntityRefLink(); return ( - - - - - - - - {profile?.email && ( - - - - )} - {description && ( - - - - )} - - - + + + + + {displayName} + + {profile?.email && ( + + {profile.email} + + )} + {description && ( + {description} + )} + + ); }; /** @public */ -export type MembersListCardClassKey = 'root' | 'cardContent' | 'memberList'; +export type MembersListCardClassKey = 'memberList'; const useListStyles = makeStyles( - theme => ({ - root: { - height: '100%', - }, - cardContent: { - overflow: 'auto', - }, + () => ({ memberList: { display: 'grid', - gap: theme.spacing(1.5), - gridTemplateColumns: `repeat(auto-fit, minmax(auto, ${theme.spacing( - 34, - )}px))`, + gap: 'var(--bui-space-3)', + gridTemplateColumns: `repeat(auto-fit, minmax(275px, 1fr))`, + gridAutoRows: '1fr', + margin: 0, + padding: 0, + paddingTop: 'var(--bui-space-3)', + listStyle: 'none', + }, + memberListItem: { + display: 'contents', }, }), { name: 'PluginOrgMembersListCardComponent' }, @@ -162,7 +157,7 @@ export const MembersListCard = (props: { }) => { const { t } = useTranslationRef(orgTranslationRef); const { - memberDisplayTitle = t('membersListCard.title'), + memberDisplayTitle, pageSize = 50, showAggregateMembersToggle, relationType = 'memberof', @@ -179,13 +174,13 @@ export const MembersListCard = (props: { const catalogApi = useApi(catalogApiRef); const displayName = profile?.displayName ?? groupName; + const cardTitle = + memberDisplayTitle ?? + t('membersListCard.title', { groupName: displayName }); const groupNamespace = grpNamespace || DEFAULT_NAMESPACE; - const [page, setPage] = useState(1); - const pageChange = (_: ChangeEvent, pageIndex: number) => { - setPage(pageIndex); - }; + const [offset, setOffset] = useState(0); const [showAggregateMembers, setShowAggregateMembers] = useState( relationAggregation === 'aggregated', @@ -194,8 +189,8 @@ export const MembersListCard = (props: { const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { - setPage(1); - }, [searchTerm]); + setOffset(0); + }, [searchTerm, showAggregateMembers]); const { loading: loadingDescendantMembers, value: descendantMembers } = useAsync(async () => { @@ -245,25 +240,6 @@ export const MembersListCard = (props: { return ; } - const nbPages = Math.ceil((members?.length || 0) / pageSize); - const paginationLabel = - nbPages < 2 - ? '' - : t('membersListCard.paginationLabel', { - page: String(page), - nbPages: String(nbPages), - }); - - const pagination = ( - - ); - const filteredMembers = members.filter(member => { const fields = [ member.metadata.name, @@ -279,72 +255,79 @@ export const MembersListCard = (props: { }); const membersToRender = searchTerm ? filteredMembers : members; + const totalCount = membersToRender.length; + const hasNextPage = offset + pageSize < totalCount; + const hasPreviousPage = offset > 0; + + const pagination = + totalCount > pageSize ? ( + setOffset(prev => prev + pageSize)} + onPreviousPage={() => setOffset(prev => Math.max(0, prev - pageSize))} + /> + ) : undefined; let memberList: JSX.Element; - if (membersToRender && membersToRender.length > 0) { + if (membersToRender.length > 0) { memberList = ( - - {membersToRender - .slice(pageSize * (page - 1), pageSize * page) - .map(member => ( - - ))} - +
      + {membersToRender.slice(offset, offset + pageSize).map(member => ( +
    • + +
    • + ))} +
    ); } else { memberList = ( - - {t('membersListCard.noMembersDescription')} + + + {searchTerm + ? t('membersListCard.noSearchResult', { searchTerm }) + : t('membersListCard.noMembersDescription')} + ); } return ( - - - {showAggregateMembersToggle && ( - <> - {t('membersListCard.aggregateMembersToggle.directMembers')} - { - setShowAggregateMembers(!showAggregateMembers); - }} - inputProps={{ - 'aria-label': t( - 'membersListCard.aggregateMembersToggle.ariaLabel', - ), - }} - /> - {t('membersListCard.aggregateMembersToggle.aggregatedMembers')} - - )} - {showAggregateMembers && loadingDescendantMembers ? ( - - ) : ( - <> - ) => - setSearchTerm(e.target.value) - } - /> - {memberList} - - )} - - + + ) + } + footerActions={pagination} + > + {showAggregateMembers && loadingDescendantMembers ? ( + + ) : ( + <> + setSearchTerm('')} + /> + {memberList} + + )} + ); }; diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 5bb058e1b2..780b9d4837 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -17,7 +17,7 @@ import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model'; import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { queryByText } from '@testing-library/react'; +import { queryByText, screen } from '@testing-library/react'; import { catalogIndexRouteRef } from '../../../routes'; import { OwnershipCard } from './OwnershipCard'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; @@ -275,7 +275,7 @@ describe('OwnershipCard', () => { it('shows relations toggle', async () => { const catalogApi = catalogApiMock({ entities: items }); - const { getByTitle } = await renderInTestApp( + await renderInTestApp( @@ -288,13 +288,15 @@ describe('OwnershipCard', () => { }, ); - expect(getByTitle('Direct Relations')).toBeInTheDocument(); + expect( + screen.getByRole('switch', { name: 'Include indirect ownership' }), + ).toBeInTheDocument(); }); it('hides relations toggle', async () => { const catalogApi = catalogApiMock({ entities: items }); - const rendered = await renderInTestApp( + await renderInTestApp( @@ -307,13 +309,13 @@ describe('OwnershipCard', () => { }, ); - expect(rendered.queryByText('Direct Relations')).toBeNull(); + expect(screen.queryByRole('switch')).not.toBeInTheDocument(); }); it('overrides relation type', async () => { const catalogApi = catalogApiMock({ entities: items }); - const { getByTitle } = await renderInTestApp( + await renderInTestApp( @@ -326,13 +328,13 @@ describe('OwnershipCard', () => { }, ); - expect(getByTitle('Aggregated Relations')).toBeInTheDocument(); + await expect(screen.findByRole('switch')).resolves.toBeChecked(); }); it('defaults to aggregated for User entity kind', async () => { const catalogApi = catalogApiMock({ entities: items }); - const { getByLabelText } = await renderInTestApp( + await renderInTestApp( @@ -345,13 +347,13 @@ describe('OwnershipCard', () => { }, ); - expect(getByLabelText('Ownership Type Switch')).toBeChecked(); + await expect(screen.findByRole('switch')).resolves.toBeChecked(); }); it('defaults to direct for all entity kinds except User', async () => { const catalogApi = catalogApiMock({ entities: items }); - const { getByLabelText } = await renderInTestApp( + await renderInTestApp( @@ -364,13 +366,13 @@ describe('OwnershipCard', () => { }, ); - expect(getByLabelText('Ownership Type Switch')).not.toBeChecked(); + expect(screen.getByRole('switch')).not.toBeChecked(); }); it('defaults to provided relationsType', async () => { const catalogApi = catalogApiMock({ entities: items }); - const { getByLabelText } = await renderInTestApp( + await renderInTestApp( @@ -383,7 +385,7 @@ describe('OwnershipCard', () => { }, ); - expect(getByLabelText('Ownership Type Switch')).not.toBeChecked(); + expect(screen.getByRole('switch')).not.toBeChecked(); }); }); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 1df55c62c2..d756fdaca3 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -14,71 +14,25 @@ * limitations under the License. */ -import { InfoCard, InfoCardVariants } from '@backstage/core-components'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemText from '@material-ui/core/ListItemText'; -import Switch from '@material-ui/core/Switch'; -import Tooltip from '@material-ui/core/Tooltip'; +import { EntityInfoCard, useEntity } from '@backstage/plugin-catalog-react'; +import { Switch } from '@backstage/ui'; import { createStyles, makeStyles } from '@material-ui/core/styles'; import { useEffect, useState } from 'react'; import { ComponentsGrid } from './ComponentsGrid'; import { EntityRelationAggregation } from '../types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { orgTranslationRef } from '../../../translation'; -import Box from '@material-ui/core/Box'; /** @public */ -export type OwnershipCardClassKey = - | 'card' - | 'cardContent' - | 'list' - | 'listItemText' - | 'listItemSecondaryAction' - | 'grid'; +export type OwnershipCardClassKey = 'grid'; const useStyles = makeStyles( - theme => + () => createStyles({ - card: { - maxHeight: '100%', - }, - cardContent: { - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', - }, - list: { - [theme.breakpoints.down('xs')]: { - padding: `0 0 12px`, - }, - }, - listItemText: { - [theme.breakpoints.down('xs')]: { - paddingRight: 0, - paddingLeft: 0, - }, - }, - listItemSecondaryAction: { - [theme.breakpoints.down('xs')]: { - width: '100%', - top: 'auto', - right: 'auto', - position: 'relative', - transform: 'unset', - }, - }, grid: { overflowY: 'auto', marginTop: 0, }, - box: { - overflowY: 'auto', - padding: theme.spacing(0, 1, 1), - margin: theme.spacing(0, -1), - }, }), { name: 'PluginOrgOwnershipCard', @@ -87,27 +41,17 @@ const useStyles = makeStyles( /** @public */ export const OwnershipCard = (props: { - variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; /** @deprecated Please use relationAggregation instead */ relationsType?: EntityRelationAggregation; relationAggregation?: EntityRelationAggregation; entityLimit?: number; - maxScrollHeight?: string; }) => { - const { - variant, - entityFilterKind, - hideRelationsToggle, - entityLimit = 6, - maxScrollHeight: propMaxScrollHeight, - } = props; + const { entityFilterKind, hideRelationsToggle, entityLimit = 6 } = props; const relationAggregation = props.relationAggregation ?? props.relationsType; const relationsToggle = hideRelationsToggle === undefined ? false : hideRelationsToggle; - const maxScrollHeight = - variant !== 'fullHeight' ? propMaxScrollHeight : undefined; const classes = useStyles(); const { entity } = useEntity(); const { t } = useTranslationRef(orgTranslationRef); @@ -125,65 +69,27 @@ export const OwnershipCard = (props: { }, [setRelationAggregation, defaultRelationAggregation, relationAggregation]); return ( - + setRelationAggregation(isSelected ? 'aggregated' : 'direct') + } + label={t('ownershipCard.aggregateRelationsToggle.label')} + /> + ) + } > - {!relationsToggle && ( - - - - - {t('ownershipCard.aggregateRelationsToggle.directRelations')} - - { - const updatedRelationAggregation = - getRelationAggregation === 'direct' - ? 'aggregated' - : 'direct'; - setRelationAggregation(updatedRelationAggregation); - }} - name="pin" - inputProps={{ - 'aria-label': t( - 'ownershipCard.aggregateRelationsToggle.ariaLabel', - ), - }} - /> - - {t('ownershipCard.aggregateRelationsToggle.aggregatedRelations')} - - - - )} - - - - + + ); }; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index 0463cc78ad..aa1bf978e6 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -55,7 +55,7 @@ export const Default = () => ( - + @@ -82,7 +82,7 @@ export const NoImage = () => ( - + @@ -134,7 +134,7 @@ export const ExtraDetails = () => ( - + diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index b5932dfb8e..45936c564f 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -51,7 +51,7 @@ describe('UserSummary Test', () => { it('Display Profile Card', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -61,10 +61,10 @@ describe('UserSummary Test', () => { ); expect(screen.getByText('calum-leavy@example.com')).toBeInTheDocument(); - expect(screen.getByAltText('Calum Leavy')).toHaveAttribute( - 'src', - 'https://example.com/staff/calum.jpeg', - ); + // BUI Avatar is decorative (aria-hidden), because the name is + // present as text right beside it. + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); + expect(screen.getByText('Calum Leavy')).toBeInTheDocument(); expect(screen.getByText('examplegroup').closest('a')).toHaveAttribute( 'href', '/catalog/default/group/examplegroup', @@ -87,7 +87,7 @@ describe('UserSummary Test', () => { it('Should limit the number of displayed groups in the card', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -109,7 +109,7 @@ describe('UserSummary Test', () => { it('Show more groups button when there are more user relations than the maximum', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -128,7 +128,7 @@ describe('UserSummary Test', () => { it('Hide more groups button when limit value is less than or equal to user relations', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -148,7 +148,7 @@ describe('UserSummary Test', () => { it('Hide all groups when max relations is equals to zero', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -193,7 +193,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -234,7 +234,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -285,7 +285,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -337,7 +337,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index ba3ee9d561..e34adde670 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,15 +19,10 @@ import { RELATION_MEMBER_OF, UserEntity, } from '@backstage/catalog-model'; -import { - Avatar, - InfoCard, - InfoCardVariants, - Link, -} from '@backstage/core-components'; +import { Link } from '@backstage/core-components'; +import { EntityInfoCard } from '@backstage/plugin-catalog-react'; +import { Avatar, Box, Flex, Text } from '@backstage/ui'; import { createStyles, makeStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Grid from '@material-ui/core/Grid'; import BaseButton from '@material-ui/core/ButtonBase'; import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; @@ -52,7 +47,6 @@ import EditIcon from '@material-ui/icons/Edit'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import { LinksGroup } from '../../Meta'; -import PersonIcon from '@material-ui/icons/Person'; import { useCallback, useState } from 'react'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; @@ -84,17 +78,21 @@ const useStyles = makeStyles( { name: 'PluginOrgUserProfileCard' }, ); -const CardTitle = (props: { title?: string }) => +const CardTitle = (props: { title: string; pictureSrc?: string }) => props.title ? ( - - - {props.title} - + + + {props.title} + ) : null; /** @public */ export const UserProfileCard = (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; @@ -132,11 +130,9 @@ export const UserProfileCard = (props: { }); return ( - } - subheader={description} - variant={props.variant} - action={ + } + headerActions={ <> {entityMetadataEditUrl && ( } > - - - - + {description && {description}} + + + {profile?.email && ( + + + + + + + + {profile.email} + + + )} - - - {profile?.email && ( - - - - - - - - {profile.email} - - - )} - - {maxRelations === undefined || maxRelations > 0 ? ( - - - - - - - - - {maxRelations && memberOfRelations.length > maxRelations ? ( - <> - , - - {t('userProfileCard.moreGroupButtonTitle', { - number: String( - memberOfRelations.length - maxRelations, - ), - })} - - - ) : null} - - - ) : null} - {props?.showLinks && } - - - + {maxRelations === undefined || maxRelations > 0 ? ( + + + + + + + + + {maxRelations && memberOfRelations.length > maxRelations ? ( + <> + , + + {t('userProfileCard.moreGroupButtonTitle', { + number: String(memberOfRelations.length - maxRelations), + })} + + + ) : null} + + + ) : null} + {props?.showLinks && } + + - + ); }; diff --git a/plugins/org/src/overridableComponents.ts b/plugins/org/src/overridableComponents.ts index b4c5f31212..a5057e57ec 100644 --- a/plugins/org/src/overridableComponents.ts +++ b/plugins/org/src/overridableComponents.ts @@ -18,7 +18,6 @@ import { Overrides } from '@material-ui/core/styles/overrides'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { ComponentsGridClassKey, - MemberComponentClassKey, MembersListCardClassKey, OwnershipCardClassKey, UserProfileCardClassKey, @@ -26,7 +25,6 @@ import { /** @public */ export type CatalogReactComponentsNameToClassKey = { - PluginOrgMemberComponent: MemberComponentClassKey; PluginOrgMembersListCardComponent: MembersListCardClassKey; PluginOrgOwnershipCard: OwnershipCardClassKey; PluginOrgComponentsGrid: ComponentsGridClassKey; diff --git a/plugins/org/src/translation.ts b/plugins/org/src/translation.ts index b7dfda6937..b5309d14e7 100644 --- a/plugins/org/src/translation.ts +++ b/plugins/org/src/translation.ts @@ -34,22 +34,18 @@ export const orgTranslationRef = createTranslationRef({ }, }, membersListCard: { - title: 'Members', - subtitle: 'of {{groupName}}', - paginationLabel: ', page {{page}} of {{nbPages}}', + cardLabel: 'User page for {{memberName}}', + title: '{{groupName}} members', noMembersDescription: 'This group has no members.', + noSearchResult: 'Found no members matching "{{searchTerm}}".', aggregateMembersToggle: { - directMembers: 'Direct Members', - aggregatedMembers: 'Aggregated Members', - ariaLabel: 'Users Type Switch', + label: 'Include subgroups', }, }, ownershipCard: { title: 'Ownership', aggregateRelationsToggle: { - directRelations: 'Direct Relations', - aggregatedRelations: 'Aggregated Relations', - ariaLabel: 'Ownership Type Switch', + label: 'Include indirect ownership', }, }, userProfileCard: { diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 51f6bbb745..088a5001e5 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + ## 0.2.16-next.0 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index 39ea2935b1..405d18a912 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.2.16-next.0", + "version": "0.2.17-next.1", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index d5975fd872..c06d588dad 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-permission-backend +## 0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-permission-node@0.10.11-next.1 + +## 0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.7.9 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + ## 0.7.8-next.0 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 6afab25c1c..ca97759c19 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.7.9-next.0", + "version": "0.7.10-next.1", "backstage": { "role": "backend-plugin", "pluginId": "permission", diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index e19fad7dd1..429a3dcdd7 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-permission-common +## 0.9.6 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features + ## 0.9.5-next.0 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 05675b6052..85607676cf 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.9.6-next.0", + "version": "0.9.6", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index b91a077e2a..fe5b29efbd 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-permission-node +## 0.10.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + +## 0.10.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 0.10.10 + +### Patch Changes + +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + ## 0.10.9-next.0 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index e9a7d2d6e7..bfcc5d5ad0 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.10.10-next.0", + "version": "0.10.11-next.1", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 33af73c41a..ccdb6b5e22 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-permission-react +## 0.4.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 0.4.40 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-common@0.9.6 + ## 0.4.40-next.1 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 3e3f7a1bf6..eb98150e64 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.40-next.1", + "version": "0.4.41-next.0", "backstage": { "role": "web-library", "pluginId": "permission", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index c1219bfc66..bc5d229d74 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-proxy-backend +## 0.6.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-proxy-node@0.1.13-next.1 + +## 0.6.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-proxy-node@0.1.13-next.0 + +## 0.6.10 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-proxy-node@0.1.12 + ## 0.6.10-next.0 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 4fd2371582..59a622929c 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.6.10-next.0", + "version": "0.6.11-next.1", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/proxy-node/CHANGELOG.md b/plugins/proxy-node/CHANGELOG.md index c2e58d8ed6..87233b9d64 100644 --- a/plugins/proxy-node/CHANGELOG.md +++ b/plugins/proxy-node/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-proxy-node +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + ## 0.1.12-next.0 ### Patch Changes diff --git a/plugins/proxy-node/package.json b/plugins/proxy-node/package.json index a052353860..26665967e6 100644 --- a/plugins/proxy-node/package.json +++ b/plugins/proxy-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-node", - "version": "0.1.12-next.0", + "version": "0.1.13-next.1", "description": "The plugin-proxy-node module for @backstage/plugin-proxy-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index 8dbaebd254..77b3f164df 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 3093bdc08e..af0c0afbc4 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.2.18-next.1", + "version": "0.2.19-next.2", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 7f671fd0ea..8def05f3e3 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + +## 0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.3.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 14741e2: Fully enable API token functionality for Bitbucket-Cloud. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 3219a09fa3..10ff9bc679 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.3.3-next.1", + "version": "0.3.4-next.2", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 98dce503f4..a32640c60c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 02f543d042..5cb8483af4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.2.18-next.1", + "version": "0.2.19-next.2", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md deleted file mode 100644 index 7461bc8f20..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ /dev/null @@ -1,1316 +0,0 @@ -# @backstage/plugin-scaffolder-backend-module-bitbucket - -## 0.3.19-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.20.0-next.1 - - @backstage/backend-plugin-api@1.7.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.3-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18-next.1 - - @backstage/plugin-scaffolder-node@0.12.5-next.1 - -## 0.3.19-next.0 - -### Patch Changes - -- 7455dae: Use node prefix on native imports -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.2-next.0 - - @backstage/backend-plugin-api@1.7.0-next.0 - - @backstage/plugin-scaffolder-node@0.12.4-next.0 - - @backstage/integration@1.19.3-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18-next.0 - - @backstage/config@1.3.6 - - @backstage/errors@1.2.7 - -## 0.3.18 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.6.1 - - @backstage/plugin-scaffolder-node@0.12.3 - - @backstage/integration@1.19.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17 - -## 0.3.18-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.19.2-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.17-next.0 - - @backstage/plugin-scaffolder-node@0.12.3-next.0 - -## 0.3.17 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.19.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.0 - - @backstage/backend-plugin-api@1.6.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16 - - @backstage/plugin-scaffolder-node@0.12.2 - -## 0.3.17-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.18.3-next.1 - - @backstage/backend-plugin-api@1.6.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.1 - - @backstage/config@1.3.6 - - @backstage/errors@1.2.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.1 - - @backstage/plugin-scaffolder-node@0.12.2-next.1 - -## 0.3.17-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.5.1-next.0 - - @backstage/integration@1.18.3-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.16-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.16-next.0 - - @backstage/plugin-scaffolder-node@0.12.2-next.0 - - @backstage/config@1.3.6 - - @backstage/errors@1.2.7 - -## 0.3.16 - -### Patch Changes - -- fa255f5: Support for Bitbucket Cloud's API token was added as `appPassword` is deprecated (no new creation from September 9, 2025) and will be removed on June 9, 2026. - - API token usage example: - - ```yaml - integrations: - bitbucketCloud: - - username: user@domain.com - token: my-token - ``` - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15 - - @backstage/integration@1.18.2 - - @backstage/backend-plugin-api@1.5.0 - - @backstage/config@1.3.6 - - @backstage/plugin-scaffolder-node@0.12.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15 - -## 0.3.16-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.5.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.1 - - @backstage/plugin-scaffolder-node@0.12.1-next.1 - -## 0.3.16-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.3.6-next.0 - - @backstage/integration@1.18.2-next.0 - - @backstage/plugin-scaffolder-node@0.12.1-next.0 - - @backstage/backend-plugin-api@1.4.5-next.0 - - @backstage/errors@1.2.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.15-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.15-next.0 - -## 0.3.15 - -### Patch Changes - -- c8aa210: Updating import for the `scaffolderActionsExtensionPoint` to be the main export -- Updated dependencies - - @backstage/integration@1.18.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.14 - - @backstage/plugin-scaffolder-node@0.12.0 - - @backstage/config@1.3.5 - - @backstage/backend-plugin-api@1.4.4 - -## 0.3.15-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.3.4-next.0 - - @backstage/integration@1.18.1-next.1 - - @backstage/backend-plugin-api@1.4.4-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.14-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14-next.1 - - @backstage/plugin-scaffolder-node@0.12.0-next.1 - -## 0.3.15-next.0 - -### Patch Changes - -- c8aa210: Updating import for the `scaffolderActionsExtensionPoint` to be the main export -- Updated dependencies - - @backstage/integration@1.18.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.14-next.0 - - @backstage/plugin-scaffolder-node@0.12.0-next.0 - - @backstage/backend-plugin-api@1.4.3 - - @backstage/config@1.3.3 - - @backstage/errors@1.2.7 - -## 0.3.14 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.18.0 - - @backstage/backend-plugin-api@1.4.3 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 - - @backstage/plugin-scaffolder-node@0.11.1 - -## 0.3.14-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.18.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13-next.0 - - @backstage/plugin-scaffolder-node@0.11.1-next.0 - - @backstage/backend-plugin-api@1.4.3-next.0 - -## 0.3.13 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.11.0 - - @backstage/backend-plugin-api@1.4.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.12 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.12 - -## 0.3.13-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.11.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.12-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.12-next.0 - - @backstage/backend-plugin-api@1.4.2-next.0 - - @backstage/config@1.3.3 - - @backstage/errors@1.2.7 - - @backstage/integration@1.17.1 - -## 0.3.12 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.3.3 - - @backstage/plugin-scaffolder-node@0.10.0 - - @backstage/integration@1.17.1 - - @backstage/backend-plugin-api@1.4.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.11 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.11 - -## 0.3.12-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.10.0-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.11-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.11-next.2 - -## 0.3.12-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.3.3-next.0 - - @backstage/integration@1.17.1-next.1 - - @backstage/backend-plugin-api@1.4.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.11-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.11-next.1 - - @backstage/plugin-scaffolder-node@0.9.1-next.1 - -## 0.3.12-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.9.1-next.0 - - @backstage/integration@1.17.1-next.0 - - @backstage/backend-plugin-api@1.4.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.11-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.11-next.0 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.11 - -### Patch Changes - -- 7f710d2: Migrating `bitbucket` actions to use the new `zod` format -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.10 - - @backstage/plugin-scaffolder-node@0.9.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.10 - - @backstage/backend-plugin-api@1.4.0 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.17.0 - -## 0.3.11-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.9.0-next.2 - - @backstage/backend-plugin-api@1.4.0-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.17.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.10-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.10-next.2 - -## 0.3.11-next.1 - -### Patch Changes - -- 7f710d2: Migrating `bitbucket` actions to use the new `zod` format -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.10-next.1 - - @backstage/plugin-scaffolder-node@0.8.3-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.10-next.1 - - @backstage/backend-plugin-api@1.4.0-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.17.0 - -## 0.3.11-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.10-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.10-next.0 - - @backstage/plugin-scaffolder-node@0.8.3-next.0 - - @backstage/backend-plugin-api@1.4.0-next.0 - -## 0.3.10 - -### Patch Changes - -- 72d019d: Removed various typos -- Updated dependencies - - @backstage/integration@1.17.0 - - @backstage/backend-plugin-api@1.3.1 - - @backstage/plugin-scaffolder-node@0.8.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.9 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.9 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.10-next.3 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.17.0-next.3 - - @backstage/plugin-scaffolder-node@0.8.2-next.3 - - @backstage/backend-plugin-api@1.3.1-next.2 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.9-next.3 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.9-next.3 - -## 0.3.10-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.17.0-next.2 - - @backstage/config@1.3.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.9-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.9-next.2 - - @backstage/plugin-scaffolder-node@0.8.2-next.2 - - @backstage/backend-plugin-api@1.3.1-next.1 - - @backstage/errors@1.2.7 - -## 0.3.10-next.1 - -### Patch Changes - -- 72d019d: Removed various typos -- Updated dependencies - - @backstage/backend-plugin-api@1.3.1-next.1 - - @backstage/integration@1.16.4-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.9-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.9-next.1 - - @backstage/plugin-scaffolder-node@0.8.2-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.10-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.16.4-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.9-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.9-next.0 - - @backstage/plugin-scaffolder-node@0.8.2-next.0 - - @backstage/backend-plugin-api@1.3.1-next.0 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.9 - -### Patch Changes - -- adfceee: Made "publish:bitbucket" action idempotent -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.8.1 - - @backstage/backend-plugin-api@1.3.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.8 - - @backstage/integration@1.16.3 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.8 - -## 0.3.9-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.16.3-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.8-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.8-next.1 - - @backstage/plugin-scaffolder-node@0.8.1-next.1 - - @backstage/backend-plugin-api@1.2.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.9-next.0 - -### Patch Changes - -- adfceee: Made "publish:bitbucket" action idempotent -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.8.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.8-next.0 - - @backstage/backend-plugin-api@1.2.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.8-next.0 - -## 0.3.8 - -### Patch Changes - -- 4f8b5b6: Allow signing git commits using configured private PGP key in scaffolder -- Updated dependencies - - @backstage/integration@1.16.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.7 - - @backstage/plugin-scaffolder-node@0.8.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.7 - - @backstage/backend-plugin-api@1.2.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.8-next.2 - -### Patch Changes - -- 4f8b5b6: Allow signing git commits using configured private PGP key in scaffolder -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.7-next.2 - - @backstage/plugin-scaffolder-node@0.8.0-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.7-next.2 - - @backstage/integration@1.16.2-next.0 - - @backstage/backend-plugin-api@1.2.1-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - -## 0.3.8-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.7.1-next.1 - - @backstage/backend-plugin-api@1.2.1-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.7-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.7-next.1 - -## 0.3.8-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.2.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.7-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.7-next.0 - - @backstage/plugin-scaffolder-node@0.7.1-next.0 - -## 0.3.7 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.2.0 - - @backstage/plugin-scaffolder-node@0.7.0 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.6 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.6 - -## 0.3.7-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.7.0-next.2 - - @backstage/backend-plugin-api@1.2.0-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.6-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.6-next.2 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.1 - -## 0.3.7-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.2.0-next.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.6-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.6-next.1 - - @backstage/plugin-scaffolder-node@0.7.0-next.1 - -## 0.3.7-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.2.0-next.0 - - @backstage/plugin-scaffolder-node@0.7.0-next.0 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/integration@1.16.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.6-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.6-next.0 - -## 0.3.6 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.6.3 - - @backstage/integration@1.16.1 - - @backstage/backend-plugin-api@1.1.1 - - @backstage/config@1.3.2 - - @backstage/errors@1.2.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.5 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.5 - -## 0.3.6-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.1.1-next.1 - - @backstage/config@1.3.2-next.0 - - @backstage/errors@1.2.7-next.0 - - @backstage/plugin-scaffolder-node@0.6.3-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.5-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.5-next.1 - - @backstage/integration@1.16.1-next.0 - -## 0.3.6-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.6.3-next.0 - - @backstage/backend-plugin-api@1.1.1-next.0 - - @backstage/config@1.3.1 - - @backstage/errors@1.2.6 - - @backstage/integration@1.16.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.5-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.5-next.0 - -## 0.3.5 - -### Patch Changes - -- 5f04976: Fixed a bug that caused missing code in published packages. -- 5c9cc05: Use native fetch instead of node-fetch -- Updated dependencies - - @backstage/integration@1.16.0 - - @backstage/backend-plugin-api@1.1.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.4 - - @backstage/plugin-scaffolder-node@0.6.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.4 - - @backstage/errors@1.2.6 - - @backstage/config@1.3.1 - -## 0.3.5-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.1.0-next.2 - - @backstage/errors@1.2.6-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.4-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.4-next.2 - - @backstage/plugin-scaffolder-node@0.6.2-next.2 - - @backstage/config@1.3.1-next.0 - - @backstage/integration@1.16.0-next.1 - -## 0.3.5-next.1 - -### Patch Changes - -- 5c9cc05: Use native fetch instead of node-fetch -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.6.2-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.4-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.4-next.1 - - @backstage/backend-plugin-api@1.1.0-next.1 - - @backstage/config@1.3.0 - - @backstage/errors@1.2.5 - - @backstage/integration@1.16.0-next.0 - -## 0.3.4-next.0 - -### Patch Changes - -- 5f04976: Fixed a bug that caused missing code in published packages. -- Updated dependencies - - @backstage/integration@1.16.0-next.0 - - @backstage/backend-plugin-api@1.0.3-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.3-next.0 - - @backstage/plugin-scaffolder-node@0.6.1-next.0 - - @backstage/config@1.3.0 - - @backstage/errors@1.2.5 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.3-next.0 - -## 0.3.2 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.3.0 - - @backstage/backend-plugin-api@1.0.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.2 - - @backstage/plugin-scaffolder-node@0.6.0 - - @backstage/errors@1.2.5 - - @backstage/integration@1.15.2 - -## 0.3.2-next.3 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.0.2-next.2 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.15.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.2-next.3 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.2-next.3 - - @backstage/plugin-scaffolder-node@0.5.1-next.3 - -## 0.3.2-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.0.2-next.2 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.15.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.2-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.2-next.2 - - @backstage/plugin-scaffolder-node@0.5.1-next.2 - -## 0.3.2-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.2-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.2-next.1 - - @backstage/backend-plugin-api@1.0.2-next.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.15.1 - - @backstage/plugin-scaffolder-node@0.5.1-next.1 - -## 0.3.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.0.2-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.15.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.2-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.2-next.0 - - @backstage/plugin-scaffolder-node@0.5.1-next.0 - -## 0.3.1 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.5.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.1 - - @backstage/integration@1.15.1 - - @backstage/backend-plugin-api@1.0.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.1 - -## 0.3.1-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.1-next.2 - - @backstage/integration@1.15.1-next.1 - - @backstage/plugin-scaffolder-node@0.5.0-next.2 - - @backstage/backend-plugin-api@1.0.1-next.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.1-next.2 - -## 0.3.1-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.15.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.1-next.1 - - @backstage/backend-plugin-api@1.0.1-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.1-next.1 - - @backstage/plugin-scaffolder-node@0.5.0-next.1 - -## 0.3.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.5.0-next.0 - - @backstage/backend-plugin-api@1.0.1-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.15.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.1-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.1-next.0 - -## 0.3.0 - -### Minor Changes - -- d425fc4: **BREAKING**: The return values from `createBackendPlugin`, `createBackendModule`, and `createServiceFactory` are now simply `BackendFeature` and `ServiceFactory`, instead of the previously deprecated form of a function that returns them. For this reason, `createServiceFactory` also no longer accepts the callback form where you provide direct options to the service. This also affects all `coreServices.*` service refs. - - This may in particular affect tests; if you were effectively doing `createBackendModule({...})()` (note the parentheses), you can now remove those extra parentheses at the end. You may encounter cases of this in your `packages/backend/src/index.ts` too, where you add plugins, modules, and services. If you were using `createServiceFactory` with a function as its argument for the purpose of passing in options, this pattern has been deprecated for a while and is no longer supported. You may want to explore the new multiton patterns to achieve your goals, or moving settings to app-config. - - As part of this change, the `IdentityFactoryOptions` type was removed, and can no longer be used to tweak that service. The identity service was also deprecated some time ago, and you will want to [migrate to the new auth system](https://backstage.io/docs/tutorials/auth-service-migration) if you still rely on it. - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@1.0.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.0 - - @backstage/integration@1.15.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-node@0.4.11 - -## 0.3.0-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.0-next.2 - - @backstage/backend-plugin-api@1.0.0-next.2 - - @backstage/integration@1.15.0-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.0-next.2 - - @backstage/plugin-scaffolder-node@0.4.11-next.2 - -## 0.3.0-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.9.0-next.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.14.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.0-next.1 - - @backstage/plugin-scaffolder-node@0.4.11-next.1 - -## 0.3.0-next.0 - -### Minor Changes - -- d425fc4: **BREAKING**: The return values from `createBackendPlugin`, `createBackendModule`, and `createServiceFactory` are now simply `BackendFeature` and `ServiceFactory`, instead of the previously deprecated form of a function that returns them. For this reason, `createServiceFactory` also no longer accepts the callback form where you provide direct options to the service. This also affects all `coreServices.*` service refs. - - This may in particular affect tests; if you were effectively doing `createBackendModule({...})()` (note the parentheses), you can now remove those extra parentheses at the end. You may encounter cases of this in your `packages/backend/src/index.ts` too, where you add plugins, modules, and services. If you were using `createServiceFactory` with a function as its argument for the purpose of passing in options, this pattern has been deprecated for a while and is no longer supported. You may want to explore the new multiton patterns to achieve your goals, or moving settings to app-config. - - As part of this change, the `IdentityFactoryOptions` type was removed, and can no longer be used to tweak that service. The identity service was also deprecated some time ago, and you will want to [migrate to the new auth system](https://backstage.io/docs/tutorials/auth-service-migration) if you still rely on it. - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.9.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.0-next.0 - - @backstage/plugin-scaffolder-node@0.4.11-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.14.0 - -## 0.2.13 - -### Patch Changes - -- 93095ee: Make sure node-fetch is version 2.7.0 or greater -- Updated dependencies - - @backstage/backend-plugin-api@0.8.0 - - @backstage/plugin-scaffolder-node@0.4.9 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13 - - @backstage/integration@1.14.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.13-next.3 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 - - @backstage/backend-plugin-api@0.8.0-next.3 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.14.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 - - @backstage/plugin-scaffolder-node@0.4.9-next.3 - -## 0.2.13-next.2 - -### Patch Changes - -- 93095ee: Make sure node-fetch is version 2.7.0 or greater -- Updated dependencies - - @backstage/backend-plugin-api@0.8.0-next.2 - - @backstage/plugin-scaffolder-node@0.4.9-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 - - @backstage/integration@1.14.0-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.13-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 - - @backstage/backend-plugin-api@0.7.1-next.1 - - @backstage/integration@1.14.0-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 - - @backstage/plugin-scaffolder-node@0.4.9-next.1 - -## 0.2.13-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/integration@1.14.0-next.0 - - @backstage/backend-plugin-api@0.7.1-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 - - @backstage/plugin-scaffolder-node@0.4.9-next.0 - -## 0.2.12 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.7.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 - - @backstage/integration@1.13.0 - - @backstage/plugin-scaffolder-node@0.4.8 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.12-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.22-next.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.13.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 - - @backstage/plugin-scaffolder-node@0.4.8-next.1 - -## 0.2.11-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.21-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.11-next.0 - - @backstage/integration@1.13.0-next.0 - - @backstage/plugin-scaffolder-node@0.4.7-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.11-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.9 - -### Patch Changes - -- 78a0b08: Internal refactor to handle `BackendFeature` contract change. -- d44a20a: Added additional plugin metadata to `package.json`. -- Updated dependencies - - @backstage/backend-plugin-api@0.6.19 - - @backstage/integration@1.12.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9 - - @backstage/plugin-scaffolder-node@0.4.5 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.9-next.2 - -### Patch Changes - -- d44a20a: Added additional plugin metadata to `package.json`. -- Updated dependencies - - @backstage/backend-plugin-api@0.6.19-next.3 - - @backstage/integration@1.12.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.2 - - @backstage/plugin-scaffolder-node@0.4.5-next.3 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.9-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.19-next.2 - - @backstage/integration@1.12.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.1 - - @backstage/plugin-scaffolder-node@0.4.5-next.2 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.9-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.19-next.0 - - @backstage/plugin-scaffolder-node@0.4.5-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.11.0 - -## 0.2.8 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.18 - - @backstage/plugin-scaffolder-node@0.4.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.8 - - @backstage/integration@1.11.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.8 - -## 0.2.8-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.4.4-next.2 - - @backstage/integration@1.11.0-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.8-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.8-next.2 - -## 0.2.8-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.4.4-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.8-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.8-next.1 - - @backstage/backend-plugin-api@0.6.18-next.1 - -## 0.2.8-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.8-next.0 - - @backstage/backend-plugin-api@0.6.18-next.0 - - @backstage/plugin-scaffolder-node@0.4.4-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.10.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.8-next.0 - -## 0.2.7 - -### Patch Changes - -- 33f958a: Improve examples to ensure consistency across all publish actions -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.7 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.7 - - @backstage/backend-plugin-api@0.6.17 - - @backstage/integration@1.10.0 - - @backstage/plugin-scaffolder-node@0.4.3 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - -## 0.2.7-next.1 - -### Patch Changes - -- 33f958a: Improve examples to ensure consistency across all publish actions -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.7-next.1 - - @backstage/backend-plugin-api@0.6.17-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.7-next.1 - - @backstage/plugin-scaffolder-node@0.4.3-next.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.10.0-next.0 - -## 0.2.7-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.7-next.0 - - @backstage/integration@1.10.0-next.0 - - @backstage/backend-plugin-api@0.6.17-next.0 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.7-next.0 - - @backstage/plugin-scaffolder-node@0.4.3-next.0 - -## 0.2.6 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.16 - - @backstage/plugin-scaffolder-node@0.4.2 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.9.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.6 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.6 - -## 0.2.5 - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.4.1 - - @backstage/backend-plugin-api@0.6.15 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/integration@1.9.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.5 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.5 - -## 0.2.4 - -### Patch Changes - -- 2bd1410: Removed unused dependencies -- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. - - It will help to maintain tests in a long run during structural changes of action context. - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.4.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.4 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.4 - - @backstage/integration@1.9.1 - - @backstage/config@1.2.0 - - @backstage/errors@1.2.4 - - @backstage/backend-plugin-api@0.6.14 - -## 0.2.4-next.2 - -### Patch Changes - -- 2bd1410: Removed unused dependencies -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.4.0-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.4-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.4-next.2 - - @backstage/integration@1.9.1-next.2 - - @backstage/backend-plugin-api@0.6.14-next.2 - - @backstage/config@1.2.0-next.1 - - @backstage/errors@1.2.4-next.0 - -## 0.2.4-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.2.0-next.1 - - @backstage/plugin-scaffolder-node@0.4.0-next.1 - - @backstage/backend-common@0.21.4-next.1 - - @backstage/backend-plugin-api@0.6.14-next.1 - - @backstage/integration@1.9.1-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.4-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.4-next.1 - - @backstage/errors@1.2.4-next.0 - -## 0.2.3-next.0 - -### Patch Changes - -- f44589d: Introduced `createMockActionContext` to unify the way of creating scaffolder mock context. - - It will help to maintain tests in a long run during structural changes of action context. - -- Updated dependencies - - @backstage/backend-common@0.21.3-next.0 - - @backstage/errors@1.2.4-next.0 - - @backstage/plugin-scaffolder-node@0.3.3-next.0 - - @backstage/backend-plugin-api@0.6.13-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.3-next.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.3-next.0 - - @backstage/config@1.1.2-next.0 - - @backstage/integration@1.9.1-next.0 - -## 0.2.0 - -### Minor Changes - -- 5eb6882: Split `@backstage/plugin-scaffolder-backend-module-bitbucket` into - `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` and - `@backstage/plugin-scaffolder-backend-module-bitbucket-server`. - - `@backstage/plugin-scaffolder-backend-module-bitbucket` was **deprecated** in favor of these two replacements. - - Please use any of the two replacements depending on your needs. - - ```diff - - backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket')); - + backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket-cloud')); - + backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket-server')); - ``` - -### Patch Changes - -- e9a5228: Exporting a default module for the new Backend System -- 8472188: Added or fixed the `repository` field in `package.json`. -- 6bb6f3e: Updated dependency `fs-extra` to `^11.2.0`. - Updated dependency `@types/fs-extra` to `^11.0.0`. -- fc98bb6: Enhanced the pull request action to allow for adding new content to the PR as described in this issue #21762 -- Updated dependencies - - @backstage/backend-common@0.21.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.0 - - @backstage/backend-plugin-api@0.6.10 - - @backstage/integration@1.9.0 - - @backstage/plugin-scaffolder-node@0.3.0 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.0 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - -## 0.2.0-next.3 - -### Patch Changes - -- 8472188: Added or fixed the `repository` field in `package.json`. -- Updated dependencies - - @backstage/backend-common@0.21.0-next.3 - - @backstage/integration@1.9.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.0-next.1 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.0-next.1 - - @backstage/plugin-scaffolder-node@0.3.0-next.3 - - @backstage/backend-plugin-api@0.6.10-next.3 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - -## 0.2.0-next.2 - -### Minor Changes - -- 5eb6882: Split `@backstage/plugin-scaffolder-backend-module-bitbucket` into - `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` and - `@backstage/plugin-scaffolder-backend-module-bitbucket-server`. - - `@backstage/plugin-scaffolder-backend-module-bitbucket` was **deprecated** in favor of these two replacements. - - Please use any of the two replacements depending on your needs. - - ```diff - - backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket')); - + backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket-cloud')); - + backend.add(import('@backstage/plugin-scaffolder-backend-module-bitbucket-server')); - ``` - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.0-next.0 - - @backstage/backend-common@0.21.0-next.2 - - @backstage/backend-plugin-api@0.6.10-next.2 - - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.0-next.0 - - @backstage/plugin-scaffolder-node@0.3.0-next.2 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - - @backstage/integration@1.9.0-next.0 - -## 0.1.2-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-plugin-api@0.6.10-next.1 - - @backstage/backend-common@0.21.0-next.1 - - @backstage/integration@1.9.0-next.0 - - @backstage/plugin-scaffolder-node@0.3.0-next.1 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - -## 0.1.2-next.0 - -### Patch Changes - -- e9a5228: Exporting a default module for the new Backend System -- fc98bb6: Enhanced the pull request action to allow for adding new content to the PR as described in this issue #21762 -- Updated dependencies - - @backstage/backend-common@0.21.0-next.0 - - @backstage/plugin-scaffolder-node@0.3.0-next.0 - - @backstage/backend-plugin-api@0.6.10-next.0 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - - @backstage/integration@1.8.0 - -## 0.1.1 - -### Patch Changes - -- a694f71: The Scaffolder builtin actions now contains an action for running pipelines from Bitbucket Cloud Rest API -- Updated dependencies - - @backstage/backend-common@0.20.1 - - @backstage/plugin-scaffolder-node@0.2.10 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - - @backstage/integration@1.8.0 - -## 0.1.1-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-common@0.20.1-next.2 - - @backstage/plugin-scaffolder-node@0.2.10-next.2 - -## 0.1.1-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-common@0.20.1-next.1 - - @backstage/integration@1.8.0 - - @backstage/config@1.1.1 - - @backstage/plugin-scaffolder-node@0.2.10-next.1 - - @backstage/errors@1.2.3 - -## 0.1.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/backend-common@0.20.1-next.0 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - - @backstage/integration@1.8.0 - - @backstage/plugin-scaffolder-node@0.2.10-next.0 - -## 0.1.0 - -### Minor Changes - -- 219d7f0: Create new scaffolder module for external integrations - -### Patch Changes - -- Updated dependencies - - @backstage/backend-common@0.20.0 - - @backstage/plugin-scaffolder-node@0.2.9 - - @backstage/integration@1.8.0 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - -## 0.1.0-next.0 - -### Minor Changes - -- 219d7f0: Create new scaffolder module for external integrations - -### Patch Changes - -- Updated dependencies - - @backstage/plugin-scaffolder-node@0.2.9-next.3 - - @backstage/backend-common@0.20.0-next.3 - - @backstage/config@1.1.1 - - @backstage/errors@1.2.3 - - @backstage/integration@1.8.0-next.1 diff --git a/plugins/scaffolder-backend-module-bitbucket/README.md b/plugins/scaffolder-backend-module-bitbucket/README.md deleted file mode 100644 index dde4207c0a..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# @backstage/plugin-scaffolder-backend-module-bitbucket - -**Deprecated!** - -Please use one of the following modules instead: - -- [@backstage/plugin-scaffolder-backend-module-bitbucket-cloud](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-bitbucket-cloud) -- [@backstage/plugin-scaffolder-backend-module-bitbucket-server](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-bitbucket-server). diff --git a/plugins/scaffolder-backend-module-bitbucket/catalog-info.yaml b/plugins/scaffolder-backend-module-bitbucket/catalog-info.yaml deleted file mode 100644 index b0a94d1c13..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/catalog-info.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: backstage-plugin-scaffolder-backend-module-bitbucket - title: '@backstage/plugin-scaffolder-backend-module-bitbucket' - description: The bitbucket module for @backstage/plugin-scaffolder-backend -spec: - lifecycle: experimental - type: backstage-backend-plugin-module - owner: maintainers diff --git a/plugins/scaffolder-backend-module-bitbucket/knip-report.md b/plugins/scaffolder-backend-module-bitbucket/knip-report.md deleted file mode 100644 index c8b2aa1788..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/knip-report.md +++ /dev/null @@ -1,8 +0,0 @@ -# Knip report - -## Unused dependencies (1) - -| Name | Location | Severity | -| :------- | :----------- | :------- | -| fs-extra | plugins/scaffolder-backend-module-bitbucket/package.json | error | - diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json deleted file mode 100644 index 2e91125500..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.3.19-next.1", - "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", - "backstage": { - "role": "backend-plugin-module", - "pluginId": "scaffolder", - "pluginPackage": "@backstage/plugin-scaffolder-backend" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "plugins/scaffolder-backend-module-bitbucket" - }, - "license": "Apache-2.0", - "exports": { - ".": "./src/index.ts", - "./package.json": "./package.json" - }, - "main": "src/index.ts", - "types": "src/index.ts", - "typesVersions": { - "*": { - "package.json": [ - "package.json" - ] - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "backstage-cli package build", - "clean": "backstage-cli package clean", - "lint": "backstage-cli package lint", - "prepack": "backstage-cli package prepack", - "postpack": "backstage-cli package postpack", - "start": "backstage-cli package start", - "test": "backstage-cli package test" - }, - "dependencies": { - "@backstage/backend-plugin-api": "workspace:^", - "@backstage/config": "workspace:^", - "@backstage/errors": "workspace:^", - "@backstage/integration": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", - "@backstage/plugin-scaffolder-node": "workspace:^", - "fs-extra": "^11.2.0", - "yaml": "^2.0.0" - }, - "devDependencies": { - "@backstage/backend-test-utils": "workspace:^", - "@backstage/cli": "workspace:^", - "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", - "msw": "^1.0.0" - }, - "deprecated": true -} diff --git a/plugins/scaffolder-backend-module-bitbucket/report.api.md b/plugins/scaffolder-backend-module-bitbucket/report.api.md deleted file mode 100644 index 33c9bcc9bd..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/report.api.md +++ /dev/null @@ -1,110 +0,0 @@ -## API Report File for "@backstage/plugin-scaffolder-backend-module-bitbucket" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; -import { Config } from '@backstage/config'; -import { ScmIntegrationRegistry } from '@backstage/integration'; -import { TemplateAction } from '@backstage/plugin-scaffolder-node'; - -// @public @deprecated -const bitbucketModule: BackendFeature; -export default bitbucketModule; - -// @public @deprecated (undocumented) -export const createBitbucketPipelinesRunAction: (options: { - integrations: ScmIntegrationRegistry; -}) => TemplateAction< - { - workspace: string; - repo_slug: string; - body?: - | { - target?: - | { - type?: string | undefined; - source?: string | undefined; - selector?: - | { - type: string; - pattern: string; - } - | undefined; - pull_request?: - | { - id: string; - } - | undefined; - commit?: - | { - type: string; - hash: string; - } - | undefined; - destination?: string | undefined; - ref_name?: string | undefined; - ref_type?: string | undefined; - destination_commit?: - | { - hash: string; - } - | undefined; - } - | undefined; - variables?: - | { - key: string; - value: string; - secured: boolean; - }[] - | undefined; - } - | undefined; - token?: string | undefined; - }, - { - buildNumber?: number | undefined; - repoUrl?: string | undefined; - pipelinesUrl?: string | undefined; - }, - 'v2' ->; - -// @public @deprecated -export function createPublishBitbucketAction(options: { - integrations: ScmIntegrationRegistry; - config: Config; -}): TemplateAction< - { - repoUrl: string; - description?: string | undefined; - repoVisibility?: 'private' | 'public' | undefined; - defaultBranch?: string | undefined; - sourcePath?: string | undefined; - enableLFS?: boolean | undefined; - token?: string | undefined; - gitCommitMessage?: string | undefined; - gitAuthorName?: string | undefined; - gitAuthorEmail?: string | undefined; - signCommit?: boolean | undefined; - }, - { - remoteUrl?: string | undefined; - repoContentsUrl?: string | undefined; - commitHash?: string | undefined; - }, - 'v2' ->; - -// @public @deprecated (undocumented) -export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction; - -// @public @deprecated (undocumented) -export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction; - -// @public @deprecated (undocumented) -export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction; -``` diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts deleted file mode 100644 index 8496405abf..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright 2023 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. - */ - -jest.mock('@backstage/plugin-scaffolder-node', () => { - return { - ...jest.requireActual('@backstage/plugin-scaffolder-node'), - initRepoAndPush: jest.fn().mockResolvedValue({ - commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', - }), - commitAndPushRepo: jest.fn().mockResolvedValue({ - commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', - }), - }; -}); - -import { createPublishBitbucketAction } from './bitbucket'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { registerMswTestHooks } from '@backstage/backend-test-utils'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import yaml from 'yaml'; -import { sep } from 'node:path'; -import { examples } from './bitbucket.examples'; -import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; - -describe('publish:bitbucket', () => { - const config = new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - { - host: 'hosted.bitbucket.com', - token: 'thing', - apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', - }, - { - host: 'notoken.bitbucket.com', - }, - ], - }, - }); - - const integrations = ScmIntegrations.fromConfig(config); - const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = createMockActionContext({ - input: { - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, - }, - }); - const server = setupServer(); - registerMswTestHooks(server); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should call initAndPush with the correct values', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[0].example).steps[0].input, - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - defaultBranch: 'master', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - commitMessage: 'initial commit', - gitAuthorInfo: { email: undefined, name: undefined }, - }); - }); - - it('should call initAndPush with the correct default branch', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[3].example).steps[0].input, - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - defaultBranch: 'main', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - commitMessage: 'initial commit', - gitAuthorInfo: { email: undefined, name: undefined }, - }); - }); - - it('should call initAndPush with the specified source path', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[4].example).steps[0].input, - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: `${mockContext.workspacePath}${sep}repoRoot`, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'initial commit', - gitAuthorInfo: { email: undefined, name: undefined }, - }); - }); - - it('should call initAndPush with the authentication token', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[6].example).steps[0].input, - }); - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'your-auth-token' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'initial commit', - gitAuthorInfo: { - email: undefined, - name: undefined, - }, - }); - }); - - it('should call initAndPush with the custom commit message', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[7].example).steps[0].input, - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'Initial commit with custom message', - gitAuthorInfo: { email: undefined, name: undefined }, - }); - }); - - it('should call initAndPush with the custom author name and email for the commit.', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[8].example).steps[0].input, - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'initial commit', - gitAuthorInfo: { email: 'your.email@example.com', name: 'Your Name' }, - }); - }); - - describe('LFS for hosted bitbucket', () => { - const repoCreationResponse = { - links: { - self: [ - { - href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: 'https://bitbucket.mycompany.com/scm/project/repo', - }, - ], - }, - }; - - it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => { - expect.assertions(1); - server.use( - rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', - (_, res, ctx) => { - return res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json(repoCreationResponse), - ); - }, - ), - rest.put( - 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer thing'); - return res(ctx.status(204)); - }, - ), - ); - - await action.handler({ - ...mockContext, - input: yaml.parse(examples[5].example).steps[0].input, - }); - }); - }); -}); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.ts deleted file mode 100644 index 6248c991f8..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; -import yaml from 'yaml'; - -export const examples: TemplateExample[] = [ - { - description: - 'Initializes a git repository with the content in the workspace, and publishes it to Bitbucket with the default configuration.', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - }, - }, - ], - }), - }, - { - description: 'Initializes a Bitbucket repository with a description.', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - description: 'Initialize a git repository', - }, - }, - ], - }), - }, - { - description: - 'Initializes a Bitbucket repository with public repo visibility, if not set defaults to private', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - repoVisibility: 'public', - }, - }, - ], - }), - }, - { - description: - 'Initializes a Bitbucket repository with a default branch, if not set defaults to master', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - defaultBranch: 'main', - }, - }, - ], - }), - }, - { - description: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - sourcePath: './repoRoot', - }, - }, - ], - }), - }, - { - description: 'Initializes a Bitbucket repository with LFS enabled', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - enableLFS: true, - }, - }, - ], - }), - }, - { - description: - 'Initializes a Bitbucket repository with a custom authentication token', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - token: 'your-auth-token', - }, - }, - ], - }), - }, - { - description: - 'Initializes a Bitbucket repository with an initial commit message, if not set defaults to `initial commit`', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - gitCommitMessage: 'Initial commit with custom message', - }, - }, - ], - }), - }, - { - description: 'Initializes a Bitbucket repository with a custom author', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - gitAuthorName: 'Your Name', - gitAuthorEmail: 'your.email@example.com', - }, - }, - ], - }), - }, - { - description: - 'Initializes a Bitbucket repository with all properties being set', - example: yaml.stringify({ - steps: [ - { - id: 'publish', - action: 'publish:bitbucket', - name: 'Publish to Bitbucket', - input: { - repoUrl: - 'bitbucket.org?repo=repo&workspace=workspace&project=project', - description: 'Initialize a git repository', - repoVisibility: 'public', - defaultBranch: 'main', - token: 'your-auth-token', - gitCommitMessage: 'Initial commit with custom message', - gitAuthorName: 'Your Name', - gitAuthorEmail: 'your.email@example.com', - }, - }, - ], - }), - }, -]; diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts deleted file mode 100644 index 654b0fffcb..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ /dev/null @@ -1,619 +0,0 @@ -/* - * Copyright 2021 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. - */ - -jest.mock('@backstage/plugin-scaffolder-node', () => { - return { - ...jest.requireActual('@backstage/plugin-scaffolder-node'), - initRepoAndPush: jest.fn().mockResolvedValue({ - commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', - }), - commitAndPushRepo: jest.fn().mockResolvedValue({ - commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', - }), - }; -}); -import { createPublishBitbucketAction } from './bitbucket'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { registerMswTestHooks } from '@backstage/backend-test-utils'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; -import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; - -describe('publish:bitbucket', () => { - const config = new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - { - host: 'hosted.bitbucket.com', - token: 'thing', - apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', - }, - { - host: 'notoken.bitbucket.com', - }, - ], - }, - }); - - const integrations = ScmIntegrations.fromConfig(config); - const action = createPublishBitbucketAction({ integrations, config }); - const mockContext = createMockActionContext({ - input: { - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - repoVisibility: 'private' as const, - }, - }); - const server = setupServer(); - registerMswTestHooks(server); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should throw an error when the repoUrl is not well formed', async () => { - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?project=project&repo=repo' }, - }), - ).rejects.toThrow(/missing workspace/); - - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?workspace=workspace&repo=repo' }, - }), - ).rejects.toThrow(/missing project/); - - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project' }, - }), - ).rejects.toThrow(/missing repo/); - }); - - it('should throw if there is no integration config provided', async () => { - await expect( - action.handler({ - ...mockContext, - input: { - repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo', - }, - }), - ).rejects.toThrow(/No matching integration configuration/); - }); - - it('should throw if there is no token in the integration config that is returned', async () => { - await expect( - action.handler({ - ...mockContext, - input: { - repoUrl: - 'notoken.bitbucket.com?workspace=workspace&project=project&repo=repo', - }, - }), - ).rejects.toThrow(/Authorization has not been provided for Bitbucket/); - }); - - it('should call the correct APIs when the host is bitbucket cloud', async () => { - expect.assertions(2); - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer tokenlols'); - expect(req.body).toEqual({ - is_private: true, - scm: 'git', - project: { key: 'project' }, - }); - return res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/repo', - }, - ], - }, - }), - ); - }, - ), - ); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', - }, - }); - }); - - it('should call the correct APIs when the host is hosted bitbucket', async () => { - expect.assertions(2); - server.use( - rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer thing'); - expect(req.body).toEqual({ public: false, name: 'repo' }); - return res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - self: [ - { - href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: 'https://bitbucket.mycompany.com/scm/project/repo', - }, - ], - }, - }), - ); - }, - ), - ); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - }, - }); - }); - - it('should work if the token is provided through ctx.input', async () => { - expect.assertions(2); - server.use( - rest.post( - 'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer lols'); - expect(req.body).toEqual({ public: false, name: 'repo' }); - return res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - self: [ - { - href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: 'https://bitbucket.mycompany.com/scm/project/repo', - }, - ], - }, - }), - ); - }, - ), - ); - await action.handler({ - ...mockContext, - input: { - repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', - token: 'lols', - }, - }); - }); - - describe('LFS for hosted bitbucket', () => { - const repoCreationResponse = { - links: { - self: [ - { - href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', - }, - ], - clone: [ - { - name: 'http', - href: 'https://bitbucket.mycompany.com/scm/project/repo', - }, - ], - }, - }; - - it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => { - expect.assertions(1); - server.use( - rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', - (_, res, ctx) => { - return res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json(repoCreationResponse), - ); - }, - ), - rest.put( - 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer thing'); - return res(ctx.status(204)); - }, - ), - ); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - enableLFS: true, - }, - }); - }); - - it('should report an error if enabling LFS fails', async () => { - server.use( - rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', - (_, res, ctx) => { - return res( - ctx.status(201), - ctx.set('Content-Type', 'application/json'), - ctx.json(repoCreationResponse), - ); - }, - ), - rest.put( - 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', - (_, res, ctx) => { - return res(ctx.status(500)); - }, - ), - ); - - await expect( - action.handler({ - ...mockContext, - input: { - ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', - enableLFS: true, - }, - }), - ).rejects.toThrow(/Failed to enable LFS/); - }); - }); - - it('should call initAndPush with the correct values', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler(mockContext); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - defaultBranch: 'master', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - commitMessage: 'initial commit', - gitAuthorInfo: {}, - }); - }); - - it('should call initAndPush with the correct default branch', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - defaultBranch: 'main', - }, - }); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - defaultBranch: 'main', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - commitMessage: 'initial commit', - gitAuthorInfo: {}, - }); - }); - - it('should call initAndPush with the configured defaultAuthor', async () => { - const customAuthorConfig = new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - { - host: 'hosted.bitbucket.com', - token: 'thing', - apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', - }, - { - host: 'notoken.bitbucket.com', - }, - ], - }, - scaffolder: { - defaultAuthor: { - name: 'Test', - email: 'example@example.com', - }, - }, - }); - - const customAuthorIntegrations = - ScmIntegrations.fromConfig(customAuthorConfig); - const customAuthorAction = createPublishBitbucketAction({ - integrations: customAuthorIntegrations, - config: customAuthorConfig, - }); - - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await customAuthorAction.handler(mockContext); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'initial commit', - gitAuthorInfo: { name: 'Test', email: 'example@example.com' }, - }); - }); - - it('should call initAndPush with the configured defaultCommitMessage', async () => { - const customAuthorConfig = new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - { - host: 'hosted.bitbucket.com', - token: 'thing', - apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', - }, - { - host: 'notoken.bitbucket.com', - }, - ], - }, - scaffolder: { - defaultCommitMessage: 'Test commit message', - }, - }); - - const customAuthorIntegrations = - ScmIntegrations.fromConfig(customAuthorConfig); - const customAuthorAction = createPublishBitbucketAction({ - integrations: customAuthorIntegrations, - config: customAuthorConfig, - }); - - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await customAuthorAction.handler(mockContext); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - defaultBranch: 'master', - commitMessage: 'initial commit', - gitAuthorInfo: { email: undefined, name: undefined }, - }); - }); - - it('should call outputs with the correct urls', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler(mockContext); - - expect(mockContext.output).toHaveBeenCalledWith( - 'remoteUrl', - 'https://bitbucket.org/workspace/cloneurl', - ); - expect(mockContext.output).toHaveBeenCalledWith( - 'repoContentsUrl', - 'https://bitbucket.org/workspace/repo/src/master', - ); - }); - - it('should call outputs with the correct urls with correct default branch', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - defaultBranch: 'main', - }, - }); - - expect(mockContext.output).toHaveBeenCalledWith( - 'remoteUrl', - 'https://bitbucket.org/workspace/cloneurl', - ); - expect(mockContext.output).toHaveBeenCalledWith( - 'repoContentsUrl', - 'https://bitbucket.org/workspace/repo/src/main', - ); - }); -}); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.ts deleted file mode 100644 index 113e642ee8..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.ts +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright 2021 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 { InputError } from '@backstage/errors'; -import { - BitbucketIntegrationConfig, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { - createTemplateAction, - getRepoSourceDirectory, - initRepoAndPush, - parseRepoUrl, -} from '@backstage/plugin-scaffolder-node'; -import { Config } from '@backstage/config'; -import { examples } from './bitbucket.examples'; - -const createBitbucketCloudRepository = async (opts: { - workspace: string; - project: string; - repo: string; - description?: string; - repoVisibility: 'private' | 'public'; - mainBranch: string; - authorization: string; - apiBaseUrl: string; -}) => { - const { - workspace, - project, - repo, - description, - repoVisibility, - mainBranch, - authorization, - apiBaseUrl, - } = opts; - - const options: RequestInit = { - method: 'POST', - body: JSON.stringify({ - scm: 'git', - description: description, - is_private: repoVisibility === 'private', - project: { key: project }, - }), - headers: { - Authorization: authorization, - 'Content-Type': 'application/json', - }, - }; - - let response: Response; - try { - response = await fetch( - `${apiBaseUrl}/repositories/${workspace}/${repo}`, - options, - ); - } catch (e) { - throw new Error(`Unable to create repository, ${e}`); - } - - if (response.status !== 200) { - throw new Error( - `Unable to create repository, ${response.status} ${ - response.statusText - }, ${await response.text()}`, - ); - } - - const r = await response.json(); - let remoteUrl = ''; - for (const link of r.links.clone) { - if (link.name === 'https') { - remoteUrl = link.href; - } - } - - // "mainbranch.name" cannot be set neither at create nor update of the repo - // the first pushed branch will be set as "main branch" then - const repoContentsUrl = `${r.links.html.href}/src/${mainBranch}`; - return { remoteUrl, repoContentsUrl }; -}; - -const createBitbucketServerRepository = async (opts: { - project: string; - repo: string; - description?: string; - repoVisibility: 'private' | 'public'; - authorization: string; - apiBaseUrl: string; -}) => { - const { - project, - repo, - description, - authorization, - repoVisibility, - apiBaseUrl, - } = opts; - - let response: Response; - const options: RequestInit = { - method: 'POST', - body: JSON.stringify({ - name: repo, - description: description, - public: repoVisibility === 'public', - }), - headers: { - Authorization: authorization, - 'Content-Type': 'application/json', - }, - }; - - try { - response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options); - } catch (e) { - throw new Error(`Unable to create repository, ${e}`); - } - - if (response.status !== 201) { - throw new Error( - `Unable to create repository, ${response.status} ${ - response.statusText - }, ${await response.text()}`, - ); - } - - const r = await response.json(); - let remoteUrl = ''; - for (const link of r.links.clone) { - if (link.name === 'http') { - remoteUrl = link.href; - } - } - - const repoContentsUrl = `${r.links.self[0].href}`; - return { remoteUrl, repoContentsUrl }; -}; - -const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => { - if (config.username && (config.token ?? config.appPassword)) { - const buffer = Buffer.from( - `${config.username}:${config.token ?? config.appPassword}`, - 'utf8', - ); - - return `Basic ${buffer.toString('base64')}`; - } - - if (config.token) { - return `Bearer ${config.token}`; - } - - throw new Error( - `Authorization has not been provided for Bitbucket. Please add either provide a username and token or username and appPassword to the Integrations config`, - ); -}; - -const performEnableLFS = async (opts: { - authorization: string; - host: string; - project: string; - repo: string; -}) => { - const { authorization, host, project, repo } = opts; - - const options: RequestInit = { - method: 'PUT', - headers: { - Authorization: authorization, - }, - }; - - const { ok, status, statusText } = await fetch( - `https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`, - options, - ); - - if (!ok) - throw new Error( - `Failed to enable LFS in the repository, ${status}: ${statusText}`, - ); -}; - -/** - * Creates a new action that initializes a git repository of the content in the workspace - * and publishes it to Bitbucket. - * @public - * @deprecated in favor of "createPublishBitbucketCloudAction" by \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud and "createPublishBitbucketServerAction" by \@backstage/plugin-scaffolder-backend-module-bitbucket-server - */ -export function createPublishBitbucketAction(options: { - integrations: ScmIntegrationRegistry; - config: Config; -}) { - const { integrations, config } = options; - - return createTemplateAction({ - id: 'publish:bitbucket', - description: - 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.', - examples, - schema: { - input: { - repoUrl: z => - z.string({ - description: 'Repository Location', - }), - description: z => - z - .string({ - description: 'Repository Description', - }) - .optional(), - repoVisibility: z => - z - .enum(['private', 'public'], { - description: 'Repository Visibility', - }) - .optional(), - defaultBranch: z => - z - .string({ - description: `Sets the default branch on the repository. The default value is 'master'`, - }) - .optional(), - sourcePath: z => - z - .string({ - description: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', - }) - .optional(), - enableLFS: z => - z - .boolean({ - description: - 'Enable LFS for the repository. Only available for hosted Bitbucket.', - }) - .optional(), - token: z => - z - .string({ - description: 'The token to use for authorization to BitBucket', - }) - .optional(), - gitCommitMessage: z => - z - .string({ - description: `Sets the commit message on the repository. The default value is 'initial commit'`, - }) - .optional(), - gitAuthorName: z => - z - .string({ - description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, - }) - .optional(), - gitAuthorEmail: z => - z - .string({ - description: `Sets the default author email for the commit.`, - }) - .optional(), - signCommit: z => - z - .boolean({ - description: 'Sign commit with configured PGP private key', - }) - .optional(), - }, - output: { - remoteUrl: z => - z - .string({ - description: 'A URL to the repository with the provider', - }) - .optional(), - repoContentsUrl: z => - z - .string({ - description: 'A URL to the root of the repository', - }) - .optional(), - commitHash: z => - z - .string({ - description: 'The git commit hash of the initial commit', - }) - .optional(), - }, - }, - async handler(ctx) { - ctx.logger.warn( - `[Deprecated] Please migrate the use of action "publish:bitbucket" to "publish:bitbucketCloud" or "publish:bitbucketServer".`, - ); - const { - repoUrl, - description, - defaultBranch = 'master', - repoVisibility = 'private', - enableLFS = false, - gitCommitMessage = 'initial commit', - gitAuthorName, - gitAuthorEmail, - signCommit, - } = ctx.input; - - const { workspace, project, repo, host } = parseRepoUrl( - repoUrl, - integrations, - ); - - // Workspace is only required for bitbucket cloud - if (host === 'bitbucket.org') { - if (!workspace) { - throw new InputError( - `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, - ); - } - } - - // Project is required for both bitbucket cloud and bitbucket server - if (!project) { - throw new InputError( - `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, - ); - } - - const integrationConfig = integrations.bitbucket.byHost(host); - - if (!integrationConfig) { - throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, - ); - } - - const authorization = getAuthorizationHeader( - ctx.input.token - ? { - host: integrationConfig.config.host, - apiBaseUrl: integrationConfig.config.apiBaseUrl, - token: ctx.input.token, - } - : integrationConfig.config, - ); - - const apiBaseUrl = integrationConfig.config.apiBaseUrl; - - const createMethod = - host === 'bitbucket.org' - ? createBitbucketCloudRepository - : createBitbucketServerRepository; - - const { remoteUrl, repoContentsUrl } = await ctx.checkpoint({ - key: `create.repo.${host}.${repo}`, - fn: async () => - createMethod({ - authorization, - workspace: workspace || '', - project, - repo, - repoVisibility, - mainBranch: defaultBranch, - description, - apiBaseUrl, - }), - }); - - const gitAuthorInfo = { - name: gitAuthorName - ? gitAuthorName - : config.getOptionalString('scaffolder.defaultAuthor.name'), - email: gitAuthorEmail - ? gitAuthorEmail - : config.getOptionalString('scaffolder.defaultAuthor.email'), - }; - const signingKey = - integrationConfig.config.commitSigningKey ?? - config.getOptionalString('scaffolder.defaultCommitSigningKey'); - if (signCommit && !signingKey) { - throw new Error( - 'Signing commits is enabled but no signing key is provided in the configuration', - ); - } - - let auth; - - if (ctx.input.token) { - auth = { - username: 'x-token-auth', - password: ctx.input.token, - }; - } else { - auth = { - username: integrationConfig.config.username - ? integrationConfig.config.username - : 'x-token-auth', - password: integrationConfig.config.appPassword - ? integrationConfig.config.appPassword - : integrationConfig.config.token ?? '', - }; - } - - const commitHash = await ctx.checkpoint({ - key: `init.repo.and.push${host}.${repo}`, - fn: async () => { - const commitResult = await initRepoAndPush({ - dir: getRepoSourceDirectory( - ctx.workspacePath, - ctx.input.sourcePath, - ), - remoteUrl, - auth, - defaultBranch, - logger: ctx.logger, - commitMessage: gitCommitMessage - ? gitCommitMessage - : config.getOptionalString('scaffolder.defaultCommitMessage'), - gitAuthorInfo, - signingKey: signCommit ? signingKey : undefined, - }); - return commitResult?.commitHash; - }, - }); - - if (enableLFS && host !== 'bitbucket.org') { - await performEnableLFS({ authorization, host, project, repo }); - } - - ctx.output('commitHash', commitHash); - ctx.output('remoteUrl', remoteUrl); - ctx.output('repoContentsUrl', repoContentsUrl); - }, - }); -} diff --git a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts b/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts deleted file mode 100644 index a9daa771f0..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2024 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 * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; - -export { createPublishBitbucketAction } from './actions/bitbucket'; - -/** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead - */ -export const createPublishBitbucketCloudAction = - bitbucketCloud.createPublishBitbucketCloudAction; - -/** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead - */ -export const createBitbucketPipelinesRunAction = - bitbucketCloud.createBitbucketPipelinesRunAction; - -/** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead - */ -export const createPublishBitbucketServerAction = - bitbucketServer.createPublishBitbucketServerAction; - -/** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead - */ -export const createPublishBitbucketServerPullRequestAction = - bitbucketServer.createPublishBitbucketServerPullRequestAction; diff --git a/plugins/scaffolder-backend-module-bitbucket/src/module.ts b/plugins/scaffolder-backend-module-bitbucket/src/module.ts deleted file mode 100644 index 884c8be004..0000000000 --- a/plugins/scaffolder-backend-module-bitbucket/src/module.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2024 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 { - coreServices, - createBackendModule, -} from '@backstage/backend-plugin-api'; -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node'; -import { - createBitbucketPipelinesRunAction, - createPublishBitbucketCloudAction, - createPublishBitbucketServerAction, - createPublishBitbucketServerPullRequestAction, -} from './deprecated'; -import { ScmIntegrations } from '@backstage/integration'; - -/** - * The Bitbucket Module for the Scaffolder Backend - * @public - * @deprecated use module by \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead - */ -export const bitbucketModule = createBackendModule({ - moduleId: 'bitbucket', - pluginId: 'scaffolder', - register({ registerInit }) { - registerInit({ - deps: { - scaffolder: scaffolderActionsExtensionPoint, - config: coreServices.rootConfig, - }, - async init({ scaffolder, config }) { - const integrations = ScmIntegrations.fromConfig(config); - - scaffolder.addActions( - createPublishBitbucketCloudAction({ integrations, config }), - createPublishBitbucketServerAction({ integrations, config }), - createPublishBitbucketServerPullRequestAction({ - integrations, - config, - }), - createBitbucketPipelinesRunAction({ integrations }), - ); - }, - }); - }, -}); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 9410753773..d58229c9ef 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.3.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.3.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index f817c1b5dc..9880d96baa 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.3.18-next.1", + "version": "0.3.19-next.2", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 147a66a961..62bce88446 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-defaults@0.16.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.3.20 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/backend-defaults@0.15.2 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.3.20-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 1cdaee4af4..6868672371 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.3.20-next.1", + "version": "0.3.21-next.2", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts index 98ea001bc1..3797651420 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'node:path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; @@ -46,16 +48,7 @@ jest.mock( describe('fetch:cookiecutter', () => { const mockDir = createMockDirectory({ mockOsTmpDir: true }); - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); + const integrations = ScmIntegrations.fromConfig(mockServices.rootConfig()); const mockTmpDir = mockDir.path; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index 2013206036..9b5dd31fc6 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'node:path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; @@ -45,16 +47,7 @@ jest.mock( describe('fetch:cookiecutter', () => { const mockDir = createMockDirectory({ mockOsTmpDir: true }); - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); + const integrations = ScmIntegrations.fromConfig(mockServices.rootConfig()); const mockTmpDir = mockDir.path; diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 4294621e01..a8cab18a81 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index 03cee892c1..5704fdf5cc 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.2.18-next.1", + "version": "0.2.19-next.2", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 376adef91d..95ae5e0c80 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index db57f5815e..e4e96b873f 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.2.18-next.1", + "version": "0.2.19-next.2", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 079057ad25..0242c3189a 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.2.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 4471dfe33e..797e4f3489 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.2.18-next.1", + "version": "0.2.19-next.2", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index d591583d73..eb45ff53af 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.9.7-next.2 + +### Patch Changes + +- b2591f6: Fixed environment `waitTime` description incorrectly asking for milliseconds instead of minutes. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.9.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.9.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.9.6 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 82ca951: cleaned up repo creation to make the unique portions explicit +- 672b972: Updated dependency `libsodium-wrappers` to `^0.8.0`. + Updated dependency `@types/libsodium-wrappers` to `^0.8.0`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.9.6-next.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index b3e7513b00..27f4241a8d 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.9.6-next.2", + "version": "0.9.7-next.2", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index b6b2d97ac8..9e930a7866 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -24,10 +24,13 @@ export function createGithubActionsDispatchAction(options: { workflowId: string; branchOrTagName: string; workflowInputs?: Record | undefined; + returnWorkflowRunDetails?: boolean | undefined; token?: string | undefined; }, { - [x: string]: any; + workflowRunId?: number | undefined; + workflowRunUrl?: string | undefined; + workflowRunHtmlUrl?: string | undefined; }, 'v2' >; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts index 5583b745d5..077af89c2a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts @@ -124,4 +124,43 @@ describe('github:actions:dispatch', () => { inputs: workflowInputs, }); }); + + it('should call createWorkflowDispatch with return_run_details when using the returnWorkflowRunDetails example', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + workflow_run_id: 123, + run_url: 'https://api.github.com/repos/owner/repo/actions/runs/123', + html_url: 'https://github.com/owner/repo/actions/runs/123', + }, + }); + + const exampleInput = yaml.parse(examples[3].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: { + ...exampleInput, + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }); + await action.handler(ctx); + + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'owner', + repo: 'repo', + return_run_details: true, + }), + ); + + expect(ctx.output).toHaveBeenCalledWith('workflowRunId', 123); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunUrl', + 'https://api.github.com/repos/owner/repo/actions/runs/123', + ); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunHtmlUrl', + 'https://github.com/owner/repo/actions/runs/123', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts index 33ad86dd89..2b8baafdbe 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts @@ -71,4 +71,21 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'GitHub Action Workflow returning run details', + example: yaml.stringify({ + steps: [ + { + action: 'github:actions:dispatch', + name: 'Dispatch GitHub Action Workflow and get run ID', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'WORKFLOW_ID', + branchOrTagName: 'main', + returnWorkflowRunDetails: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts index 94a51e5b4e..fc8cfcce0b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -130,4 +130,64 @@ describe('github:actions:dispatch', () => { inputs: workflowInputs, }); }); + + it('should call createWorkflowDispatch with return_run_details when returnWorkflowRunDetails is true', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + workflow_run_id: 123, + run_url: 'https://api.github.com/repos/owner/repo/actions/runs/123', + html_url: 'https://github.com/owner/repo/actions/runs/123', + }, + }); + + const ctx = Object.assign({}, mockContext, { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'dispatch_workflow', + branchOrTagName: 'main', + returnWorkflowRunDetails: true, + }, + }); + await action.handler(ctx); + + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'owner', + repo: 'repo', + workflow_id: 'dispatch_workflow', + ref: 'main', + return_run_details: true, + }), + ); + + expect(ctx.output).toHaveBeenCalledWith('workflowRunId', 123); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunUrl', + 'https://api.github.com/repos/owner/repo/actions/runs/123', + ); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunHtmlUrl', + 'https://github.com/owner/repo/actions/runs/123', + ); + }); + + it('should not set outputs when returnWorkflowRunDetails is false', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: undefined, + }); + + const ctx = Object.assign({}, mockContext, { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'dispatch_workflow', + branchOrTagName: 'main', + returnWorkflowRunDetails: false, + }, + }); + await action.handler(ctx); + + expect(ctx.output).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index f35cbcc04c..00dd72a9e3 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { InputError } from '@backstage/errors'; +import { assertError, InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrations, @@ -62,7 +62,14 @@ export function createGithubActionsDispatchAction(options: { z .record(z.string(), { description: - 'Inputs keys and values to send to GitHub Action configured on the workflow file. The maximum number of properties is 10.', + 'Inputs keys and values to send to GitHub Action configured on the workflow file. The maximum number of properties is 25.', + }) + .optional(), + returnWorkflowRunDetails: z => + z + .boolean({ + description: + 'If true, returns the workflow run ID and URLs as action outputs.', }) .optional(), token: z => @@ -73,6 +80,14 @@ export function createGithubActionsDispatchAction(options: { }) .optional(), }, + output: { + workflowRunId: z => + z.number({ description: 'The triggered workflow run ID' }).optional(), + workflowRunUrl: z => + z.string({ description: 'API URL of the workflow run' }).optional(), + workflowRunHtmlUrl: z => + z.string({ description: 'HTML URL of the workflow run' }).optional(), + }, }, async handler(ctx) { const { @@ -80,6 +95,7 @@ export function createGithubActionsDispatchAction(options: { workflowId, branchOrTagName, workflowInputs, + returnWorkflowRunDetails, token: providedToken, } = ctx.input; @@ -106,20 +122,65 @@ export function createGithubActionsDispatchAction(options: { log: ctx.logger, }); - await ctx.checkpoint({ - key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`, - fn: async () => { - await client.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id: workflowId, - ref: branchOrTagName, - inputs: workflowInputs, - }); + try { + const runDetails = await ctx.checkpoint({ + key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`, + fn: async () => { + const dispatchParams = { + owner, + repo, + workflow_id: workflowId, + ref: branchOrTagName, + inputs: workflowInputs, + ...(returnWorkflowRunDetails ? { return_run_details: true } : {}), + }; + const response = await client.rest.actions.createWorkflowDispatch( + dispatchParams, + ); - ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); - }, - }); + ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); + + if (returnWorkflowRunDetails && response.data) { + // GitHub's API returns 200 with run details when return_run_details is true. + // @octokit/openapi-types still types this as OctokitResponse because + // it hasn't picked up the updated GitHub OpenAPI spec yet. + // See: https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/ + // This cast can be removed once @octokit/openapi-types includes the updated spec. + // Note: only supported on GitHub.com and GitHub Enterprise Cloud, not GitHub Enterprise Server + const data = response.data as unknown as { + workflow_run_id: number; + run_url: string; + html_url: string; + }; + + return { + workflowRunId: data.workflow_run_id, + workflowRunUrl: data.run_url, + workflowRunHtmlUrl: data.html_url, + }; + } + return null; + }, + }); + + if (runDetails) { + ctx.output('workflowRunId', runDetails.workflowRunId); + ctx.output('workflowRunUrl', runDetails.workflowRunUrl); + ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); + + if (runDetails.workflowRunHtmlUrl) { + ctx.logger.info( + `Workflow run url: ${runDetails.workflowRunHtmlUrl}`, + ); + } + } + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${e.message}`, + ); + throw e; + } }, }); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 4a36be3b5b..7e2c880b5d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -111,7 +111,7 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin z .number({ description: - 'The time to wait before creating or updating the environment (in milliseconds)', + 'The time to wait before creating or updating the environment (in minutes)', }) .optional(), preventSelfReview: z => diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index 571ed5219c..6f404578fc 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.11.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.11.4-next.1 + +### Patch Changes + +- 0c1726a: Added new `gitlab:group:access` scaffolder action to add or remove users and groups as members of GitLab groups. The action supports specifying members via `userIds` and/or `groupIds` array parameters, configurable access levels (Guest, Reporter, Developer, Maintainer, Owner), and defaults to the 'add' action when not specified. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.11.4-next.0 + +### Patch Changes + +- 5730c8e: Added `maskedAndHidden` option to `gitlab:projectVariable:create` and `publish:gitlab` action to support creating GitLab project variables that are both masked and hidden. Updated gitbeaker to version 43.8.0 for proper type support. +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.11.3 + +### Patch Changes + +- 6b5e7d9: Allow setting optional description on group creation +- 7455dae: Use node prefix on native imports +- f0f9403: Changed `gitlab:group:ensureExists` action to use `Groups.show` API instead of `Groups.search` for checking if a group path exists. This is more efficient as it directly retrieves the group by path rather than searching and filtering results. +- 32c51c0: Added new `gitlab:user:info` scaffolder action that retrieves information about a GitLab user. The action can fetch either the current authenticated user or a specific user by ID. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.11.3-next.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 4542ec4198..1579f2e55b 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -117,6 +117,7 @@ spec: value: "${{ steps['gitlab-access-token'].output.access_token }}" variableType: 'env_var' masked: true + maskedAndHidden: false variableProtected: false raw: false environmentScope: '*' diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index d5a35090e9..28517d25f6 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.11.3-next.2", + "version": "0.11.4-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", @@ -50,8 +50,9 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@gitbeaker/requester-utils": "^41.2.0", - "@gitbeaker/rest": "^41.2.0", + "@gitbeaker/core": "^43.8.0", + "@gitbeaker/requester-utils": "^43.8.0", + "@gitbeaker/rest": "^43.8.0", "luxon": "^3.0.0", "yaml": "^2.0.0", "zod": "^3.25.76" diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index 6b59166118..cbe31b02f3 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -8,6 +8,28 @@ import { Config } from '@backstage/config'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +// @public +export const createGitlabGroupAccessAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction< + { + repoUrl: string; + path: string | number; + token?: string | undefined; + userIds?: number[] | undefined; + groupIds?: number[] | undefined; + action?: 'add' | 'remove' | undefined; + accessLevel?: string | number | undefined; + }, + { + userIds?: number[] | undefined; + groupIds?: number[] | undefined; + path?: string | number | undefined; + accessLevel?: number | undefined; + }, + 'v2' +>; + // @public export const createGitlabGroupEnsureExistsAction: (options: { integrations: ScmIntegrationRegistry; @@ -111,6 +133,7 @@ export const createGitlabProjectVariableAction: (options: { token?: string | undefined; variableProtected?: boolean | undefined; masked?: boolean | undefined; + maskedAndHidden?: boolean | undefined; raw?: boolean | undefined; environmentScope?: string | undefined; }, @@ -221,6 +244,7 @@ export function createPublishGitlabAction(options: { variable_type?: 'file' | 'env_var' | undefined; masked?: boolean | undefined; environment_scope?: string | undefined; + masked_and_hidden?: boolean | undefined; }[] | undefined; }, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts index 1e3ac40d35..564075fe96 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.test.ts @@ -146,6 +146,19 @@ describe('publish:gitlab', () => { ], }, }); + const mockContextWithMaskedAndHiddenVariable = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + repoVisibility: 'private' as const, + projectVariables: [ + { + key: 'secret', + value: 'secret-value', + masked_and_hidden: true, + }, + ], + }, + }); beforeEach(() => { jest.resetAllMocks(); @@ -441,6 +454,36 @@ describe('publish:gitlab', () => { variableType: 'env_var', protected: true, masked: true, + masked_and_hidden: false, + raw: false, + environmentScope: '*', + }, + ); + }); + + it('should call the correct Gitlab APIs for variables with masked_and_hidden option', async () => { + mockGitlabClient.Users.showCurrentUser.mockResolvedValue({ id: 12345 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ + id: 1234, + kind: 'group', + }); + mockGitlabClient.Groups.allProjects.mockResolvedValue([]); + mockGitlabClient.Projects.create.mockResolvedValue({ + id: 123456, + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler(mockContextWithMaskedAndHiddenVariable); + + expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( + 123456, + 'secret', + 'secret-value', + { + variableType: 'env_var', + protected: false, + masked: false, + masked_and_hidden: true, raw: false, environmentScope: '*', }, @@ -725,8 +768,8 @@ describe('publish:gitlab', () => { expect(mockGitlabClient.Users.showCurrentUser).toHaveBeenCalled(); expect(mockGitlabClient.ProjectMembers.add).toHaveBeenCalledWith( 123456, - 12345, 50, + { userId: 12345 }, ); expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ namespaceId: 1234, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts index 7f3f673260..d743dd06ac 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlab.ts @@ -206,6 +206,7 @@ export function createPublishGitlabAction(options: { variable_type: z.enum(['env_var', 'file']).optional(), protected: z.boolean().optional(), masked: z.boolean().optional(), + masked_and_hidden: z.boolean().optional(), raw: z.boolean().optional(), environment_scope: z.string().optional(), }), @@ -339,7 +340,7 @@ export function createPublishGitlabAction(options: { token: integrationConfig.config.token, }); - await adminClient.ProjectMembers.add(projectId, userId, 50); + await adminClient.ProjectMembers.add(projectId, 50, { userId }); } const remoteUrl = (http_url_to_repo as string).replace(/\.git$/, ''); @@ -435,6 +436,7 @@ export function createPublishGitlabAction(options: { 'env_var') as VariableType, protected: variable.protected ?? false, masked: variable.masked ?? false, + masked_and_hidden: variable.masked_and_hidden ?? false, raw: variable.raw ?? false, environment_scope: variable.environment_scope ?? '*', }); @@ -448,6 +450,7 @@ export function createPublishGitlabAction(options: { variableType: variableWithDefaults.variable_type, protected: variableWithDefaults.protected, masked: variableWithDefaults.masked, + masked_and_hidden: variableWithDefaults.masked_and_hidden, environmentScope: variableWithDefaults.environment_scope, description: variableWithDefaults.description, raw: variableWithDefaults.raw, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.test.ts new file mode 100644 index 0000000000..f49986a344 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2026 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 { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import yaml from 'yaml'; +import { createGitlabGroupAccessAction } from './gitlabGroupAccessAction'; +import { examples } from './gitlabGroupAccessAction.examples'; +import { mockServices } from '@backstage/backend-test-utils'; + +const mockGitlabClient = { + GroupMembers: { + add: jest.fn(), + remove: jest.fn(), + }, + Groups: { + share: jest.fn(), + unshare: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:group:access examples', () => { + const mockContext = createMockActionContext(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const config = mockServices.rootConfig({ + data: { + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupAccessAction({ integrations }); + + it(`Should ${examples[0].description}`, async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 789, + }); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it(`Should ${examples[1].description}`, async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[1].example).steps[0].input, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith( + 'group1', + 30, + { userId: 456 }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 'group1'); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it(`Should ${examples[2].description}`, async () => { + mockGitlabClient.GroupMembers.remove.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[2].example).steps[0].input, + }); + + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 456); + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 789); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).not.toHaveBeenCalledWith( + 'accessLevel', + expect.anything(), + ); + }); + + it(`Should ${examples[3].description}`, async () => { + mockGitlabClient.Groups.share.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[3].example).steps[0].input, + }); + + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 456, + 30, + {}, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it(`Should ${examples[4].description}`, async () => { + mockGitlabClient.Groups.unshare.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[4].example).steps[0].input, + }); + + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledWith(123, 456, {}); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).not.toHaveBeenCalledWith( + 'accessLevel', + expect.anything(), + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.ts new file mode 100644 index 0000000000..8ff69f3b9d --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.examples.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2026 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Add users to a group using numeric group ID', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroupAccess', + name: 'Add Users to Group', + action: 'gitlab:group:access', + input: { + repoUrl: 'gitlab.com', + path: 123, + userIds: [456, 789], + accessLevel: 30, + }, + }, + ], + }), + }, + { + description: 'Add users to a group using string path', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroupAccess', + name: 'Add Users to Group', + action: 'gitlab:group:access', + input: { + repoUrl: 'gitlab.com', + path: 'group1', + userIds: [456], + accessLevel: 'developer', + }, + }, + ], + }), + }, + { + description: 'Remove users from a group', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroupAccess', + name: 'Remove Users from Group', + action: 'gitlab:group:access', + input: { + repoUrl: 'gitlab.com', + path: 123, + userIds: [456, 789], + action: 'remove', + }, + }, + ], + }), + }, + { + description: 'Share a group with another group', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroupAccess', + name: 'Share Group', + action: 'gitlab:group:access', + input: { + repoUrl: 'gitlab.com', + path: 123, + groupIds: [456], + accessLevel: 30, + }, + }, + ], + }), + }, + { + description: 'Unshare a group', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroupAccess', + name: 'Unshare Group', + action: 'gitlab:group:access', + input: { + repoUrl: 'gitlab.com', + path: 123, + groupIds: [456], + action: 'remove', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.test.ts new file mode 100644 index 0000000000..04491c3c4e --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.test.ts @@ -0,0 +1,690 @@ +/* + * Copyright 2026 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 { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabGroupAccessAction } from './gitlabGroupAccessAction'; +import { getClient } from '../util'; +import { mockServices } from '@backstage/backend-test-utils'; + +const mockGitlabClient = { + GroupMembers: { + add: jest.fn(), + edit: jest.fn(), + remove: jest.fn(), + }, + Groups: { + share: jest.fn(), + unshare: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +jest.mock('../util', () => ({ + getClient: jest.fn().mockImplementation(() => mockGitlabClient), + parseRepoHost: (repoUrl: string) => repoUrl, +})); + +describe('gitlab:group:access', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const config = mockServices.rootConfig({ + data: { + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupAccessAction({ integrations }); + + const mockContext = createMockActionContext(); + + // User tests + it('should add a single user to a group with the specified access level', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({ + id: 1, + user_id: 456, + group_id: 123, + access_level: 30, + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should add multiple users to a group with the specified access level', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456, 789, 101], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledTimes(3); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 789, + }); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 101, + }); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789, 101]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should default to add action when action is not specified', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockGitlabClient.GroupMembers.remove).not.toHaveBeenCalled(); + }); + + it('should remove a single user from a group', async () => { + mockGitlabClient.GroupMembers.remove.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + action: 'remove', + }, + }); + + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 456); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).not.toHaveBeenCalledWith( + 'accessLevel', + expect.anything(), + ); + }); + + it('should remove multiple users from a group', async () => { + mockGitlabClient.GroupMembers.remove.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456, 789], + action: 'remove', + }, + }); + + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 456); + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 789); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + }); + + it('should default to accessLevel 30 (Developer) when not specified', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should not call API on dryRun for add action', async () => { + await action.handler({ + ...mockContext, + isDryRun: true, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456, 789], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).not.toHaveBeenCalled(); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should not call API on dryRun for remove action', async () => { + await action.handler({ + ...mockContext, + isDryRun: true, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + action: 'remove', + }, + }); + + expect(mockGitlabClient.GroupMembers.remove).not.toHaveBeenCalled(); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).not.toHaveBeenCalledWith( + 'accessLevel', + expect.anything(), + ); + }); + + it('should use the token from the integration config when none is provided', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + }, + }); + + expect(getClient).toHaveBeenCalledWith( + expect.not.objectContaining({ + token: expect.anything(), + }), + ); + }); + + it('should use a provided token for authentication', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + token: 'mysecrettoken', + }, + }); + + expect(getClient).toHaveBeenCalledWith( + expect.objectContaining({ + token: 'mysecrettoken', + }), + ); + }); + + it('should add users as Guest (accessLevel 10)', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 10, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 10, { + userId: 456, + }); + }); + + it('should add users as Maintainer (accessLevel 40)', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 40, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 40, { + userId: 456, + }); + }); + + it('should add users as Owner (accessLevel 50)', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 50, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 50, { + userId: 456, + }); + }); + + it('should accept string accessLevel "developer"', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 'developer', + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should accept string accessLevel "maintainer" (case insensitive)', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 'MAINTAINER', + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 40, { + userId: 456, + }); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 40); + }); + + it('should throw an error for invalid string accessLevel', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 'invalid_level', + }, + }), + ).rejects.toThrow('Invalid access level: "invalid_level"'); + + expect(mockGitlabClient.GroupMembers.add).not.toHaveBeenCalled(); + }); + + it('should handle 409 conflict by editing existing user member', async () => { + mockGitlabClient.GroupMembers.add.mockRejectedValue({ + cause: { response: { status: 409 } }, + }); + mockGitlabClient.GroupMembers.edit.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockGitlabClient.GroupMembers.edit).toHaveBeenCalledWith( + 123, + 456, + 30, + ); + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + }); + + // Group sharing tests + it('should share a single group with another group', async () => { + mockGitlabClient.Groups.share.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 456, + 30, + {}, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should share multiple groups with a group', async () => { + mockGitlabClient.Groups.share.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456, 789], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.Groups.share).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 456, + 30, + {}, + ); + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 789, + 30, + {}, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should unshare groups from a group', async () => { + mockGitlabClient.Groups.unshare.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456, 789], + action: 'remove', + }, + }); + + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledWith(123, 456, {}); + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledWith(123, 789, {}); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + }); + + it('should handle 409 conflict for group sharing by re-sharing', async () => { + mockGitlabClient.Groups.share.mockRejectedValueOnce({ + cause: { response: { status: 409 } }, + }); + mockGitlabClient.Groups.unshare.mockResolvedValue(undefined); + mockGitlabClient.Groups.share.mockResolvedValueOnce({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.Groups.share).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledWith(123, 456, {}); + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456]); + }); + + it('should not call API on dryRun for group sharing', async () => { + await action.handler({ + ...mockContext, + isDryRun: true, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456, 789], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.Groups.share).not.toHaveBeenCalled(); + + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + // Mixed mode tests + it('should add users and share groups simultaneously', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + mockGitlabClient.Groups.share.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456, 789], + groupIds: [101, 102], + accessLevel: 30, + }, + }); + + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 456, + }); + expect(mockGitlabClient.GroupMembers.add).toHaveBeenCalledWith(123, 30, { + userId: 789, + }); + + expect(mockGitlabClient.Groups.share).toHaveBeenCalledTimes(2); + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 101, + 30, + {}, + ); + expect(mockGitlabClient.Groups.share).toHaveBeenCalledWith( + 123, + 102, + 30, + {}, + ); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456, 789]); + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [101, 102]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + expect(mockContext.output).toHaveBeenCalledWith('accessLevel', 30); + }); + + it('should remove users and unshare groups simultaneously', async () => { + mockGitlabClient.GroupMembers.remove.mockResolvedValue(undefined); + mockGitlabClient.Groups.unshare.mockResolvedValue(undefined); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + groupIds: [789], + action: 'remove', + }, + }); + + expect(mockGitlabClient.GroupMembers.remove).toHaveBeenCalledWith(123, 456); + expect(mockGitlabClient.Groups.unshare).toHaveBeenCalledWith(123, 789, {}); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [789]); + expect(mockContext.output).toHaveBeenCalledWith('path', 123); + }); + + // Validation tests + it('should throw an error when neither userIds nor groupIds provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + }, + }), + ).rejects.toThrow( + 'At least one of userIds or groupIds must be provided and non-empty', + ); + + expect(mockGitlabClient.GroupMembers.add).not.toHaveBeenCalled(); + expect(mockGitlabClient.Groups.share).not.toHaveBeenCalled(); + }); + + it('should throw an error when userIds and groupIds are both empty arrays', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [], + groupIds: [], + }, + }), + ).rejects.toThrow( + 'At least one of userIds or groupIds must be provided and non-empty', + ); + + expect(mockGitlabClient.GroupMembers.add).not.toHaveBeenCalled(); + expect(mockGitlabClient.Groups.share).not.toHaveBeenCalled(); + }); + + it('should not output userIds when only groupIds are provided', async () => { + mockGitlabClient.Groups.share.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + groupIds: [456], + accessLevel: 30, + }, + }); + + expect(mockContext.output).not.toHaveBeenCalledWith( + 'userIds', + expect.anything(), + ); + expect(mockContext.output).toHaveBeenCalledWith('groupIds', [456]); + }); + + it('should not output groupIds when only userIds are provided', async () => { + mockGitlabClient.GroupMembers.add.mockResolvedValue({}); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: 123, + userIds: [456], + accessLevel: 30, + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith('userIds', [456]); + expect(mockContext.output).not.toHaveBeenCalledWith( + 'groupIds', + expect.anything(), + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.ts new file mode 100644 index 0000000000..7ad295880e --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupAccessAction.ts @@ -0,0 +1,259 @@ +/* + * Copyright 2026 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 { InputError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { AccessLevel } from '@gitbeaker/core'; +import { getClient, parseRepoHost } from '../util'; +import { examples } from './gitlabGroupAccessAction.examples'; + +type NonAdminAccessLevel = Exclude; + +const accessLevelMapping: Record = { + no_access: 0, + minimal_access: 5, + guest: 10, + planner: 15, + reporter: 20, + developer: 30, + maintainer: 40, + owner: 50, +}; + +function resolveAccessLevel(level: string | number): number { + if (typeof level === 'number') return level; + const resolved = accessLevelMapping[level.toLocaleLowerCase('en-US')]; + if (resolved === undefined) { + throw new InputError( + `Invalid access level: "${level}". Valid values are: ${Object.keys( + accessLevelMapping, + ).join(', ')} or a numeric GitLab access level`, + ); + } + return resolved; +} + +/** + * Creates a `gitlab:group:access` Scaffolder action. + * + * @public + */ +export const createGitlabGroupAccessAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + + return createTemplateAction({ + id: 'gitlab:group:access', + description: 'Adds or removes users and groups from a GitLab group', + supportsDryRun: true, + examples, + schema: { + input: { + repoUrl: z => + z.string({ + description: + "The host of the GitLab instance, for example 'gitlab.com' or 'gitlab.my-company.com'.", + }), + token: z => + z + .string({ + description: 'The token to use for authorization to GitLab', + }) + .optional(), + path: z => + z.union([z.number(), z.string()], { + description: + 'The ID or path of the group to add/remove members from', + }), + userIds: z => + z + .array(z.number(), { + description: 'The IDs of the users to add/remove', + }) + .optional(), + groupIds: z => + z + .array(z.number(), { + description: + 'The IDs of the groups to share with or unshare from the target group', + }) + .optional(), + action: z => + z + .enum(['add', 'remove'], { + description: + 'The action to perform: add or remove the members. Defaults to "add".', + }) + .default('add') + .optional(), + accessLevel: z => + z + .union([z.number(), z.string()], { + description: + 'The access level for the members. Can be a number (0=No access, 5=Minimal access, 10=Guest, 15=Planner, 20=Reporter, 30=Developer, 40=Maintainer, 50=Owner) or a string (e.g., "guest", "developer"). Defaults to 30 (Developer).', + }) + .default(30) + .optional(), + }, + output: { + userIds: z => + z + .array(z.number(), { + description: 'The IDs of the users that were added or removed', + }) + .optional(), + groupIds: z => + z + .array(z.number(), { + description: + 'The IDs of the groups that were shared with or unshared from', + }) + .optional(), + path: z => + z + .union([z.number(), z.string()], { + description: + 'The ID or path of the group the members were added to or removed from', + }) + .optional(), + accessLevel: z => + z + .number({ + description: + 'The access level granted to the members (only for add action)', + }) + .optional(), + }, + }, + async handler(ctx) { + const { + token, + repoUrl, + path, + userIds = [], + groupIds = [], + accessLevel: rawAccessLevel = 30, + action = 'add', + } = ctx.input; + + if (userIds.length === 0 && groupIds.length === 0) { + throw new InputError( + 'At least one of userIds or groupIds must be provided and non-empty', + ); + } + + const accessLevel = resolveAccessLevel(rawAccessLevel); + + if (ctx.isDryRun) { + if (userIds.length > 0) { + ctx.output('userIds', userIds); + } + if (groupIds.length > 0) { + ctx.output('groupIds', groupIds); + } + ctx.output('path', path); + if (action === 'add') { + ctx.output('accessLevel', accessLevel); + } + return; + } + + const host = parseRepoHost(repoUrl); + + const api = getClient({ host, integrations, token }); + + // Process users + for (const userId of userIds) { + ctx.logger.info( + `${action === 'add' ? 'Adding' : 'Removing'} user ${userId} ${ + action === 'add' ? 'to' : 'from' + } group ${path}`, + ); + await ctx.checkpoint({ + key: `gitlab.group.member.user.${action}.${path}.${userId}`, + fn: async () => { + if (action === 'add') { + try { + await api.GroupMembers.add( + path, + accessLevel as NonAdminAccessLevel, + { userId }, + ); + } catch (error: any) { + // If member already exists, try to edit instead + if (error.cause?.response?.status === 409) { + await api.GroupMembers.edit( + path, + userId, + accessLevel as NonAdminAccessLevel, + ); + return; + } + throw error; + } + } else { + await api.GroupMembers.remove(path, userId); + } + }, + }); + } + + // Process groups + for (const sharedGroupId of groupIds) { + ctx.logger.info( + `${action === 'add' ? 'Adding' : 'Removing'} group ${sharedGroupId} ${ + action === 'add' ? 'to' : 'from' + } group ${path}`, + ); + await ctx.checkpoint({ + key: `gitlab.group.member.group.${action}.${path}.${sharedGroupId}`, + fn: async () => { + if (action === 'add') { + try { + await api.Groups.share(path, sharedGroupId, accessLevel, {}); + } catch (error: any) { + // If group is already shared, unshare and re-share + if (error.cause?.response?.status === 409) { + await api.Groups.unshare(path, sharedGroupId, {}); + await api.Groups.share(path, sharedGroupId, accessLevel, {}); + return; + } + throw error; + } + } else { + await api.Groups.unshare(path, sharedGroupId, {}); + } + }, + }); + } + + ctx.output('path', path); + + if (userIds.length > 0) { + ctx.output('userIds', userIds); + } + if (groupIds.length > 0) { + ctx.output('groupIds', groupIds); + } + + if (action === 'add') { + ctx.output('accessLevel', accessLevel); + } + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts index 99b26682d8..be844f83e3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts @@ -85,6 +85,7 @@ describe('gitlab:projectVariableAction: create examples', () => { variableType: 'env_var', // Correctly using variableType environmentScope: '*', masked: false, + masked_and_hidden: false, protected: false, raw: false, }, @@ -109,6 +110,7 @@ describe('gitlab:projectVariableAction: create examples', () => { variableType: 'file', // Correctly using variableType protected: false, masked: false, + masked_and_hidden: false, raw: false, environmentScope: '*', }, @@ -131,6 +133,7 @@ describe('gitlab:projectVariableAction: create examples', () => { 'my_value', { masked: false, + masked_and_hidden: false, raw: false, environmentScope: '*', variableType: 'env_var', // Correctly using variableType @@ -159,6 +162,7 @@ describe('gitlab:projectVariableAction: create examples', () => { environmentScope: '*', variableType: 'env_var', // Correctly using variableType masked: true, + masked_and_hidden: false, }, ); }); @@ -182,6 +186,7 @@ describe('gitlab:projectVariableAction: create examples', () => { environmentScope: '*', variableType: 'env_var', // Correctly using variableType masked: false, + masked_and_hidden: false, raw: true, }, ); @@ -205,6 +210,7 @@ describe('gitlab:projectVariableAction: create examples', () => { protected: false, variableType: 'env_var', // Correctly using variableType masked: false, + masked_and_hidden: false, raw: false, environmentScope: 'production', }, @@ -229,6 +235,32 @@ describe('gitlab:projectVariableAction: create examples', () => { protected: false, variableType: 'env_var', // Correctly using variableType masked: false, + masked_and_hidden: false, + raw: false, + environmentScope: '*', + }, + ); + }); + + it(`Should ${examples[7].description}`, async () => { + mockGitlabClient.ProjectVariables.create.mockResolvedValue({ + token: 'TOKEN', + }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[7].example).steps[0].input, + }); + + expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( + '999', + 'SECRET_TOKEN', + 'super-secret-token', + { + protected: false, + variableType: 'env_var', + masked: false, + masked_and_hidden: true, raw: false, environmentScope: '*', }, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.ts index 81d526f7c8..e1d040f028 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.ts @@ -157,4 +157,24 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab project variable that is masked and hidden.', + example: yaml.stringify({ + steps: [ + { + id: 'createVariable', + action: 'gitlab:projectVariable:create', + name: 'Create GitLab Project Variable', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '999', + key: 'SECRET_TOKEN', + value: 'super-secret-token', + variableType: 'env_var', + maskedAndHidden: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts index 824a8566f7..031ff028bb 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts @@ -78,6 +78,13 @@ export const createGitlabProjectVariableAction = (options: { }) .default(false) .optional(), + maskedAndHidden: z => + z + .boolean({ + description: 'Whether the variable is masked and hidden', + }) + .default(false) + .optional(), raw: z => z .boolean({ @@ -103,6 +110,7 @@ export const createGitlabProjectVariableAction = (options: { variableType, variableProtected = false, masked = false, + maskedAndHidden = false, raw = false, environmentScope = '*', token, @@ -119,6 +127,7 @@ export const createGitlabProjectVariableAction = (options: { variableType: variableType as VariableType, protected: variableProtected, masked, + masked_and_hidden: maskedAndHidden, raw, environmentScope, }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts index 2a74e78a41..17364e9916 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/index.ts @@ -15,6 +15,7 @@ */ export * from './gitlab'; export * from './gitlabGroupEnsureExists'; +export * from './gitlabGroupAccessAction'; export * from './gitlabIssueCreate'; export * from './gitlabIssueEdit'; export * from './gitlabMergeRequest'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/module.ts b/plugins/scaffolder-backend-module-gitlab/src/module.ts index 61525753a2..653ce6e377 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/module.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/module.ts @@ -21,6 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { scaffolderAutocompleteExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; import { createGitlabGroupEnsureExistsAction, + createGitlabGroupAccessAction, createGitlabIssueAction, createGitlabProjectAccessTokenAction, createGitlabProjectDeployTokenAction, @@ -55,6 +56,7 @@ export const gitlabModule = createBackendModule({ scaffolder.addActions( createGitlabGroupEnsureExistsAction({ integrations }), + createGitlabGroupAccessAction({ integrations }), createGitlabProjectMigrateAction({ integrations }), createGitlabIssueAction({ integrations }), createGitlabProjectAccessTokenAction({ integrations }), diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index fe77988c78..d5e408f931 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.23 + ## 0.1.19-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index f56864d918..3bd2a51d86 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.1.19-next.0", + "version": "0.1.20-next.2", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index be3dda0d5e..6537c9122b 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.5.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.5.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.5.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.5.18 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.5.18-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 2523d27f9e..3d12d55f1c 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.5.18-next.1", + "version": "0.5.19-next.2", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts index 751751642a..5ad1eba2fb 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -27,12 +27,14 @@ jest.mock('./railsNewRunner', () => { }; }); -import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'node:path'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './index.examples'; import yaml from 'yaml'; @@ -41,16 +43,7 @@ import { ContainerRunner } from './ContainerRunner'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); + const integrations = ScmIntegrations.fromConfig(mockServices.rootConfig()); const mockContext = createMockActionContext({ input: { diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index c9b4ad2646..a538066f59 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -27,12 +27,14 @@ jest.mock('./railsNewRunner', () => { }; }); -import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'node:path'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'node:stream'; import { UrlReaderService } from '@backstage/backend-plugin-api'; @@ -40,16 +42,7 @@ import { ContainerRunner } from './ContainerRunner'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, - { host: 'myazurehostnotoken.com' }, - ], - }, - }), - ); + const integrations = ScmIntegrations.fromConfig(mockServices.rootConfig()); const mockContext = createMockActionContext({ input: { diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 4d778b6a08..889f5f9f72 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.3.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.3.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 1f68cdff96..b61df2d060 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.3.1-next.0", + "version": "0.3.2-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index b86d43bc20..2c6d89ca0a 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.4.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.2 + +## 0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.1 + +## 0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.0 + +## 0.4.19 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-scaffolder-node-test-utils@0.3.8 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.4.19-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 972e9a36f2..05256ea20b 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.4.19-next.0", + "version": "0.4.20-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 00b5919933..4309e492d4 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,104 @@ # @backstage/plugin-scaffolder-backend +## 3.2.0-next.2 + +### Minor Changes + +- e8736ea: Added secrets schema validation for task creation, retry, and dry-run endpoints. When a template defines `spec.secrets.schema`, the API validates provided secrets against the schema and returns a `400` error if validation fails. + +### Patch Changes + +- 30ff981: Fixed a security vulnerability where secrets could bypass log redaction when transformed through Nunjucks filters in scaffolder templates. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 3.2.0-next.1 + +### Minor Changes + +- c9b11eb: Added a new `list-scaffolder-tasks` action that allows querying scaffolder tasks with optional ownership filtering and pagination support +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 7695dd2: Added a new `list-scaffolder-actions` action that returns all installed scaffolder actions with their schemas and examples + +### Patch Changes + +- e27bd4e: Removed `@backstage/plugin-scaffolder-backend-module-bitbucket` from `package.json` as the package itself has been deprecated and the code deleted. +- ccc20cf: create scaffolder MCP action to dry run a provided scaffolder template +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 3.1.4-next.0 + +### Patch Changes + +- 4e39e63: Removed unused dependencies +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 3.1.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Removed unused dependency +- 0ce78b0: Support `if` conditions inside `each` loops for scaffolder steps +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- 1e669cc: Migrate audit events reference docs to http://backstage.io/docs. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.11.3 + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.17 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.19 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.18 + - @backstage/plugin-scaffolder-backend-module-github@0.9.6 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.18 + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.7 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + - @backstage/plugin-events-node@0.4.19 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.18 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.18 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 3.1.3-next.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/knip-report.md b/plugins/scaffolder-backend/knip-report.md index eb8ddd05aa..2661c35327 100644 --- a/plugins/scaffolder-backend/knip-report.md +++ b/plugins/scaffolder-backend/knip-report.md @@ -1,27 +1,2 @@ # Knip report -## Unused dependencies (14) - -| Name | Location | Severity | -| :--------------------------------------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-server | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-gerrit | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-github | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitlab | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-azure | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitea | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-bitbucket-cloud-common | plugins/scaffolder-backend/package.json | error | -| @backstage/plugin-auth-node | plugins/scaffolder-backend/package.json | error | -| concat-stream | plugins/scaffolder-backend/package.json | error | -| p-limit | plugins/scaffolder-backend/package.json | error | -| tar | plugins/scaffolder-backend/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | plugins/scaffolder-backend/package.json | error | - diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cb7af14507..fc5396377e 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "3.1.3-next.2", + "version": "3.2.0-next.2", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", @@ -71,27 +71,15 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-auth-node": "workspace:^", - "@backstage/plugin-bitbucket-cloud-common": "workspace:^", - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-azure": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-bitbucket": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-github": "workspace:^", - "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "@opentelemetry/api": "^1.9.0", "@types/luxon": "^3.0.0", - "concat-stream": "^2.0.0", "express": "^4.22.0", "fs-extra": "^11.2.0", "globby": "^11.0.0", @@ -103,7 +91,6 @@ "logform": "^2.3.2", "luxon": "^3.0.0", "nunjucks": "^3.2.3", - "p-limit": "^3.1.0", "p-queue": "^6.6.2", "prom-client": "^15.0.0", "triple-beam": "^1.4.1", @@ -116,7 +103,6 @@ "zod-to-json-schema": "^3.25.1" }, "devDependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 39bab85c7e..17997da485 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -23,6 +23,7 @@ import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import { eventsServiceRef } from '@backstage/plugin-events-node'; import { scaffolderActionsExtensionPoint, + scaffolderServiceRef, TaskBroker, TemplateAction, } from '@backstage/plugin-scaffolder-node'; @@ -59,7 +60,11 @@ import { convertFiltersToRecord, convertGlobalsToRecord, } from './util/templating'; -import { actionsServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + actionsServiceRef, + actionsRegistryServiceRef, +} from '@backstage/backend-plugin-api/alpha'; +import { createScaffolderActions } from './actions'; /** * Scaffolder plugin @@ -144,6 +149,8 @@ export const scaffolderPlugin = createBackendPlugin({ catalog: catalogServiceRef, events: eventsServiceRef, actionsRegistry: actionsServiceRef, + actionsRegistryService: actionsRegistryServiceRef, + scaffolderService: scaffolderServiceRef, }, async init({ logger, @@ -159,6 +166,8 @@ export const scaffolderPlugin = createBackendPlugin({ events, auditor, actionsRegistry, + actionsRegistryService, + scaffolderService, }) { const log = loggerToWinstonLogger(logger); const integrations = ScmIntegrations.fromConfig(config); @@ -211,6 +220,12 @@ export const scaffolderPlugin = createBackendPlugin({ `Starting scaffolder with the following actions enabled ${actionIds}`, ); + createScaffolderActions({ + actionsRegistry: actionsRegistryService, + scaffolderService, + auth, + }); + const router = await createRouter({ logger, config, diff --git a/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.test.ts b/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.test.ts new file mode 100644 index 0000000000..cdd69d1541 --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.test.ts @@ -0,0 +1,267 @@ +/* + * Copyright 2026 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 { createDryRunTemplateAction } from './createDryRunTemplateAction'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils'; + +type DryRunTemplateOutput = { + valid: boolean; + message: string; + errors?: string[]; + log?: Array<{ message: string; stepId?: string; status?: string }>; + output?: Record; + steps?: Array<{ id: string; name: string; action: string }>; +}; + +const validTemplateYaml = ` +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: test-template + namespace: default + title: Test Template +spec: + type: service + steps: + - id: step-1 + name: Step One + action: debug:log + input: + message: hello +`; + +const invalidYaml = ` +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: test + invalid: yaml: syntax: error +spec: + type: service + steps: [ +`; + +describe('createDryRunTemplateAction', () => { + const mockScaffolderService = scaffolderServiceMock.mock(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return success with logs when dry-run succeeds', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const dryRunResult = { + log: [ + { + body: { + message: 'Step completed', + stepId: 'step-1', + status: 'completed' as const, + }, + }, + ], + output: { result: 'ok' }, + steps: [{ id: 'step-1', name: 'Step One', action: 'debug:log' }], + directoryContents: [], + }; + + mockScaffolderService.dryRun.mockResolvedValue(dryRunResult); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { templateYaml: validTemplateYaml }, + }); + + expect(result.output).toEqual({ + valid: true, + message: 'Template validation successful', + log: [ + { + message: 'Step completed', + stepId: 'step-1', + status: 'completed', + }, + ], + output: { result: 'ok' }, + steps: [{ id: 'step-1', name: 'Step One', action: 'debug:log' }], + }); + + expect(mockScaffolderService.dryRun).toHaveBeenCalledWith( + { + template: expect.objectContaining({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: expect.objectContaining({ + name: 'test-template', + }), + }), + values: {}, + directoryContents: [], + }, + { credentials: expect.anything() }, + ); + }); + + it('should pass values and files to the scaffolder service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockScaffolderService.dryRun.mockResolvedValue({ + log: [], + output: {}, + steps: [], + directoryContents: [], + }); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const values = { name: 'my-app' }; + const files = [ + { + path: 'README.md', + content: 'hello', + }, + ]; + + await mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { + templateYaml: validTemplateYaml, + values, + files, + }, + }); + + expect(mockScaffolderService.dryRun).toHaveBeenCalledWith( + { + template: expect.any(Object), + values, + directoryContents: [ + { + path: 'README.md', + base64Content: Buffer.from('hello').toString('base64'), + }, + ], + }, + { credentials: expect.anything() }, + ); + }); + + it('should return validation errors when YAML is invalid', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { templateYaml: invalidYaml }, + }); + + expect(result.output).toEqual({ + valid: false, + message: 'Failed to parse YAML template', + errors: expect.arrayContaining([ + expect.stringContaining('YAML parsing error'), + ]), + }); + + expect(mockScaffolderService.dryRun).not.toHaveBeenCalled(); + }); + + it('should propagate errors from the scaffolder service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + + mockScaffolderService.dryRun.mockRejectedValue( + new Error('Authentication error'), + ); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { templateYaml: validTemplateYaml }, + }), + ).rejects.toThrow('Authentication error'); + }); + + it('should use default empty values and files when not provided', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockScaffolderService.dryRun.mockResolvedValue({ + log: [], + output: {}, + steps: [], + directoryContents: [], + }); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + await mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { templateYaml: validTemplateYaml }, + }); + + expect(mockScaffolderService.dryRun).toHaveBeenCalledWith( + { + template: expect.any(Object), + values: {}, + directoryContents: [], + }, + { credentials: expect.anything() }, + ); + }); + + it('should map log entries from body fields', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockScaffolderService.dryRun.mockResolvedValue({ + log: [{ body: { message: 'Plain log message' } }], + output: {}, + steps: [], + directoryContents: [], + }); + + createDryRunTemplateAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:dry-run-template', + input: { templateYaml: validTemplateYaml }, + }); + + const output = result.output as DryRunTemplateOutput; + expect(output.valid).toBe(true); + expect(output.log).toEqual([ + { message: 'Plain log message', stepId: undefined, status: undefined }, + ]); + }); +}); diff --git a/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.ts b/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.ts new file mode 100644 index 0000000000..a96e52fb4d --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createDryRunTemplateAction.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2026 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { ScaffolderService } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; +import yaml from 'yaml'; + +const MAX_CONTENT_SIZE = 64 * 1024; + +function base64EncodeContent(content: string): string { + if (content.length > MAX_CONTENT_SIZE) { + return Buffer.from('', 'utf8').toString('base64'); + } + return Buffer.from(content, 'utf8').toString('base64'); +} + +export const createDryRunTemplateAction = ({ + actionsRegistry, + scaffolderService, +}: { + actionsRegistry: ActionsRegistryService; + scaffolderService: ScaffolderService; +}) => { + actionsRegistry.register({ + name: 'dry-run-template', + title: 'Dry Run Scaffolder Template', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: + 'Dry-runs a scaffolder template to validate it without making changes. Returns success with execution logs, or errors for validation failures.', + schema: { + input: z => + z.object({ + templateYaml: z + .string() + .describe( + 'The YAML content of the scaffolder template to validate', + ), + values: z + .record(z.unknown()) + .optional() + .describe('Input values for template parameters'), + files: z + .array( + z.object({ + path: z + .string() + .describe('File path relative to template root'), + content: z.string().describe('File content'), + }), + ) + .optional() + .describe('Files required for running the template'), + }), + output: z => + z.object({ + valid: z.boolean().describe('Whether the template is valid'), + message: z + .string() + .describe('Success message or validation error details'), + errors: z + .array(z.string()) + .optional() + .describe('List of validation errors'), + log: z + .array( + z.object({ + message: z.string(), + stepId: z.string().optional(), + status: z.string().optional(), + }), + ) + .optional() + .describe('Execution log from dry-run'), + output: z + .record(z.unknown()) + .optional() + .describe('Template output values'), + steps: z + .array( + z.object({ + id: z.string(), + name: z.string(), + action: z.string(), + }), + ) + .optional() + .describe('Parsed template steps'), + }), + }, + action: async ({ input, credentials }) => { + const { templateYaml, values = {}, files = [] } = input; + + let template; + try { + template = yaml.parse(templateYaml); + } catch (parseError) { + const yamlError = parseError as yaml.YAMLParseError; + return { + output: { + valid: false, + message: 'Failed to parse YAML template', + errors: [ + `YAML parsing error: ${yamlError.message}`, + yamlError.linePos + ? `At line ${yamlError.linePos[0].line}, column ${yamlError.linePos[0].col}` + : '', + ].filter(Boolean), + }, + }; + } + + const result = await scaffolderService.dryRun( + { + template, + values: values as JsonObject, + directoryContents: files.map(file => ({ + path: file.path, + base64Content: base64EncodeContent(file.content), + })), + }, + { credentials }, + ); + + return { + output: { + valid: true, + message: 'Template validation successful', + log: result.log?.map(entry => ({ + message: entry.body.message, + stepId: entry.body.stepId, + status: entry.body.status, + })), + output: result.output, + steps: result.steps, + }, + }; + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.test.ts b/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.test.ts new file mode 100644 index 0000000000..7ddf498d9d --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright 2026 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 { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils'; +import { LogEvent } from '@backstage/plugin-scaffolder-common'; +import { createGetScaffolderTaskLogsAction } from './createGetScaffolderTaskLogsAction'; + +describe('createGetScaffolderTaskLogsAction', () => { + it('should return log events for a task', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + + const mockEvents: LogEvent[] = [ + { + id: 1, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:00Z', + type: 'log', + body: { message: 'Starting step', stepId: 'step-1' }, + }, + { + id: 2, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:01Z', + type: 'log', + body: { message: 'Step complete', stepId: 'step-1' }, + }, + { + id: 3, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:02Z', + type: 'completion', + body: { message: 'Task completed', status: 'completed' }, + }, + ]; + + mockScaffolderService.getLogs.mockResolvedValue(mockEvents); + + createGetScaffolderTaskLogsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:get-scaffolder-task-logs', + input: { taskId: 'task-1' }, + }); + + expect(result.output).toEqual({ + events: [ + { + id: 1, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:00Z', + type: 'log', + body: { + message: 'Starting step', + stepId: 'step-1', + status: undefined, + }, + }, + { + id: 2, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:01Z', + type: 'log', + body: { + message: 'Step complete', + stepId: 'step-1', + status: undefined, + }, + }, + { + id: 3, + taskId: 'task-1', + createdAt: '2025-01-01T00:00:02Z', + type: 'completion', + body: { + message: 'Task completed', + stepId: undefined, + status: 'completed', + }, + }, + ], + }); + expect(mockScaffolderService.getLogs).toHaveBeenCalledWith( + { taskId: 'task-1', after: undefined }, + expect.objectContaining({ credentials: expect.anything() }), + ); + }); + + it('should pass the after parameter through to the service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + + mockScaffolderService.getLogs.mockResolvedValue([]); + + createGetScaffolderTaskLogsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:get-scaffolder-task-logs', + input: { taskId: 'task-2', after: 42 }, + }); + + expect(result.output).toEqual({ events: [] }); + expect(mockScaffolderService.getLogs).toHaveBeenCalledWith( + { taskId: 'task-2', after: 42 }, + expect.objectContaining({ credentials: expect.anything() }), + ); + }); + + it('should throw when the service call fails', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + + mockScaffolderService.getLogs.mockRejectedValue( + new Error('Internal Server Error'), + ); + + createGetScaffolderTaskLogsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:get-scaffolder-task-logs', + input: { taskId: 'task-3' }, + }), + ).rejects.toThrow('Internal Server Error'); + }); +}); diff --git a/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.ts b/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.ts new file mode 100644 index 0000000000..08eb3a504f --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createGetScaffolderTaskLogsAction.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2026 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { ScaffolderService } from '@backstage/plugin-scaffolder-node'; + +export const createGetScaffolderTaskLogsAction = ({ + actionsRegistry, + scaffolderService, +}: { + actionsRegistry: ActionsRegistryService; + scaffolderService: ScaffolderService; +}) => { + actionsRegistry.register({ + name: 'get-scaffolder-task-logs', + title: 'Get Scaffolder Task Logs', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: ` +Retrieve the log events for a scaffolder task. +Each log event has a type (log, completion, cancelled, or recovered), a body containing a message and optional step ID and status. +Use the after parameter to fetch only events after a specific event ID for incremental polling. + `, + schema: { + input: z => + z.object({ + taskId: z.string().describe('The ID of the scaffolder task'), + after: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Return only log events after this event ID for incremental polling', + ), + }), + output: z => + z + .object({ + events: z + .array( + z.object({ + id: z.number().describe('The event ID'), + taskId: z + .string() + .describe('The ID of the task this event belongs to'), + createdAt: z + .string() + .describe('Timestamp when the event was created'), + type: z + .string() + .describe( + 'Event type: log, completion, cancelled, or recovered', + ), + body: z + .object({ + message: z.string().describe('The log message'), + stepId: z + .string() + .optional() + .describe('The step ID associated with this event'), + status: z + .string() + .optional() + .describe('The task status at the time of this event'), + }) + .describe('The event body'), + }), + ) + .describe('The list of log events for the task'), + }) + .describe('Object containing the events array'), + }, + action: async ({ input, credentials }) => { + const events = await scaffolderService.getLogs( + { taskId: input.taskId, after: input.after }, + { credentials }, + ); + + return { + output: { + events: events.map(event => ({ + id: event.id, + taskId: event.taskId, + createdAt: event.createdAt, + type: event.type, + body: { + message: event.body.message, + stepId: event.body.stepId, + status: event.body.status, + }, + })), + }, + }; + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.test.ts b/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.test.ts new file mode 100644 index 0000000000..145e3deeed --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2026 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 { createListScaffolderActionsAction } from './createListScaffolderActionsAction'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils'; +import type { ListActionsResponse } from '@backstage/plugin-scaffolder-common'; + +type ListActionsOutput = { + actions: Array<{ + id: string; + description: string; + schema: { input: object; output: object }; + examples: Array<{ description: string; example: string }>; + }>; +}; + +describe('createListScaffolderActionsAction', () => { + it('should list all scaffolder actions sorted by id with full properties', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + mockScaffolderService.listActions.mockResolvedValue(createMockActions()); + + createListScaffolderActionsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-actions', + input: {}, + }); + + const output = result.output as ListActionsOutput; + expect(output.actions).toHaveLength(3); + + const actionIds = output.actions.map(a => a.id); + expect(actionIds).toEqual([ + 'catalog:register', + 'debug:log', + 'fetch:template', + ]); + + expect(output.actions[0]).toEqual({ + id: 'catalog:register', + description: 'Registers entities in the catalog', + schema: { + input: { type: 'object' }, + output: { type: 'object' }, + }, + examples: [{ description: 'Basic usage', example: 'register entity' }], + }); + }); + + it('should handle actions without descriptions, schemas, or examples', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + mockScaffolderService.listActions.mockResolvedValue([ + { id: 'minimal-action' }, + ]); + + createListScaffolderActionsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-actions', + input: {}, + }); + + const output = result.output as ListActionsOutput; + expect(output.actions).toHaveLength(1); + expect(output.actions[0]).toEqual({ + id: 'minimal-action', + description: '', + schema: { input: {}, output: {} }, + examples: [], + }); + }); + + it('should return empty array when no actions are registered', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + mockScaffolderService.listActions.mockResolvedValue([]); + + createListScaffolderActionsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-actions', + input: {}, + }); + + const output = result.output as ListActionsOutput; + expect(output.actions).toEqual([]); + }); + + it('should propagate errors from the scaffolder service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + mockScaffolderService.listActions.mockRejectedValue( + new Error('Service unavailable'), + ); + + createListScaffolderActionsAction({ + actionsRegistry: mockActionsRegistry, + scaffolderService: mockScaffolderService, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-actions', + input: {}, + }), + ).rejects.toThrow('Service unavailable'); + }); +}); + +function createMockActions(): ListActionsResponse { + return [ + { + id: 'fetch:template', + description: 'Fetches a template', + schema: { + input: { type: 'object' }, + output: { type: 'object' }, + }, + examples: [], + }, + { + id: 'catalog:register', + description: 'Registers entities in the catalog', + schema: { + input: { type: 'object' }, + output: { type: 'object' }, + }, + examples: [{ description: 'Basic usage', example: 'register entity' }], + }, + { + id: 'debug:log', + description: 'Logs debug information', + schema: { + input: { type: 'object' }, + output: { type: 'object' }, + }, + examples: [], + }, + ]; +} diff --git a/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.ts b/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.ts new file mode 100644 index 0000000000..4450207e16 --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/createListScaffolderActionsAction.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2026 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { ScaffolderService } from '@backstage/plugin-scaffolder-node'; + +export const createListScaffolderActionsAction = ({ + actionsRegistry, + scaffolderService, +}: { + actionsRegistry: ActionsRegistryService; + scaffolderService: ScaffolderService; +}) => { + actionsRegistry.register({ + name: 'list-scaffolder-actions', + title: 'List Scaffolder Actions', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: `Lists all installed Scaffolder actions. +Each action includes: +- id: The action identifier +- description: What the action does +- schema: Input and output JSON schemas +- examples: Usage examples when available`, + schema: { + input: z => z.object({}).describe('No input is required'), + output: z => + z.object({ + actions: z.array( + z.object({ + id: z.string(), + description: z.string(), + schema: z.object({ + input: z + .object({}) + .passthrough() + .describe('JSON Schema for input of Action'), + output: z + .object({}) + .passthrough() + .describe('JSON Schema for output of Action'), + }), + examples: z.array(z.any()).optional(), + }), + ), + }), + }, + action: async ({ credentials }) => { + const actions = await scaffolderService.listActions(undefined, { + credentials, + }); + const scaffolderActions = actions.map(action => ({ + id: action.id, + description: action.description ?? '', + schema: { + input: { ...(action.schema?.input ?? {}) }, + output: { ...(action.schema?.output ?? {}) }, + }, + examples: action.examples ?? [], + })); + return { + output: { + actions: scaffolderActions.sort((a, b) => a.id.localeCompare(b.id)), + }, + }; + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/actions/index.ts b/plugins/scaffolder-backend/src/actions/index.ts new file mode 100644 index 0000000000..2dee76d1da --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2026 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { createListScaffolderTasksAction } from './listScaffolderTasksAction'; +import { ScaffolderService } from '@backstage/plugin-scaffolder-node'; +import { createDryRunTemplateAction } from './createDryRunTemplateAction'; +import { createListScaffolderActionsAction } from './createListScaffolderActionsAction'; +import { createGetScaffolderTaskLogsAction } from './createGetScaffolderTaskLogsAction'; + +export const createScaffolderActions = (options: { + actionsRegistry: ActionsRegistryService; + scaffolderService: ScaffolderService; + auth: AuthService; +}) => { + createListScaffolderTasksAction({ + actionsRegistry: options.actionsRegistry, + auth: options.auth, + scaffolderService: options.scaffolderService, + }); + createDryRunTemplateAction(options); + createListScaffolderActionsAction(options); + createGetScaffolderTaskLogsAction(options); +}; diff --git a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts new file mode 100644 index 0000000000..075ba919f1 --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.test.ts @@ -0,0 +1,268 @@ +/* + * 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 { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { mockServices, mockCredentials } from '@backstage/backend-test-utils'; +import { NotAllowedError } from '@backstage/errors'; +import { ScaffolderTask } from '@backstage/plugin-scaffolder-common'; +import { scaffolderServiceMock } from '@backstage/plugin-scaffolder-node/testUtils'; +import { createListScaffolderTasksAction } from './listScaffolderTasksAction'; +import { ListTasksResponse } from '../schema/openapi/generated/models/ListTasksResponse.model'; + +describe('createListScaffolderTasksAction', () => { + it('should list tasks successfully', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + const mockTasks = generateMockTasks(); + + mockScaffolderService.listTasks.mockResolvedValue({ + items: mockTasks.tasks.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })) as ScaffolderTask[], + totalItems: mockTasks.totalTasks ?? 0, + }); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: {}, + }); + + const expectedTasks: ScaffolderTask[] = mockTasks.tasks.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })); + + expect(result.output).toEqual({ + tasks: expectedTasks, + totalTasks: mockTasks.totalTasks ?? 0, + }); + expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( + { createdBy: undefined, limit: undefined, offset: undefined }, + expect.objectContaining({ credentials: expect.anything() }), + ); + }); + + it('should pass limit and offset through to the API and return paginated results', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + const paginatedTasks = [ + { + id: 'task-2', + spec: {}, + status: 'completed', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:01:00Z', + }, + { + id: 'task-3', + spec: {}, + status: 'processing', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:02:00Z', + }, + ]; + + mockScaffolderService.listTasks.mockResolvedValue({ + items: paginatedTasks as ScaffolderTask[], + totalItems: 10, + }); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: { limit: 2, offset: 1 }, + }); + + expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( + { createdBy: undefined, limit: 2, offset: 1 }, + expect.objectContaining({ credentials: expect.anything() }), + ); + + const expectedTasks = paginatedTasks.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })); + + expect(result.output).toEqual({ + tasks: expectedTasks, + totalTasks: 10, + }); + }); + + it('should throw an error if the service call fails', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + + mockScaffolderService.listTasks.mockRejectedValue( + new Error('Internal Server Error'), + ); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: {}, + }), + ).rejects.toThrow('Internal Server Error'); + }); + + it('should use createdBy filter when owned is true with user identity', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + const mockTasks = generateMockTasks(); + + mockAuth.isPrincipal.mockImplementation( + (creds, type) => + type === 'user' && + (creds?.principal as { type?: string })?.type === 'user' && + typeof (creds.principal as { userEntityRef?: string }).userEntityRef === + 'string', + ); + mockScaffolderService.listTasks.mockResolvedValue({ + items: mockTasks.tasks.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })) as ScaffolderTask[], + totalItems: mockTasks.totalTasks ?? 0, + }); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + await mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: { owned: true }, + credentials: mockCredentials.user('user:default/alice'), + }); + + expect(mockScaffolderService.listTasks).toHaveBeenCalledWith( + { + createdBy: 'user:default/alice', + limit: undefined, + offset: undefined, + }, + expect.objectContaining({ credentials: expect.anything() }), + ); + }); + + it('should throw NotAllowedError when owned is true without user identity', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockAuth = mockServices.auth.mock(); + const mockScaffolderService = scaffolderServiceMock.mock(); + + mockAuth.isPrincipal.mockReturnValue(false); + + createListScaffolderTasksAction({ + actionsRegistry: mockActionsRegistry, + auth: mockAuth, + scaffolderService: mockScaffolderService, + }); + + await expect( + mockActionsRegistry.invoke({ + id: 'test:list-scaffolder-tasks', + input: { owned: true }, + credentials: mockCredentials.service(), + }), + ).rejects.toThrow(NotAllowedError); + + expect(mockScaffolderService.listTasks).not.toHaveBeenCalled(); + }); +}); + +// Return a mocked ListTasksResponse that contains a number of different mocked tasks +function generateMockTasks(): ListTasksResponse { + return { + tasks: [ + { + id: 'task-1', + spec: {}, + status: 'completed', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:01:00Z', + createdBy: 'user:default/guest', + }, + { + id: 'task-2', + spec: {}, + status: 'completed', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:01:00Z', + createdBy: 'user:default/guest', + }, + { + id: 'task-3', + spec: {}, + status: 'processing', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:02:00Z', + createdBy: 'user:default/admin', + }, + { + id: 'task-4', + spec: {}, + status: 'failed', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:02:00Z', + createdBy: 'user:default/admin', + }, + { + id: 'task-5', + spec: {}, + status: 'cancelled', + createdAt: '2025-01-01T00:00:00Z', + lastHeartbeatAt: '2025-01-01T00:02:00Z', + createdBy: 'user:default/admin', + }, + ], + totalTasks: 5, + }; +} diff --git a/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts new file mode 100644 index 0000000000..70977c9d12 --- /dev/null +++ b/plugins/scaffolder-backend/src/actions/listScaffolderTasksAction.ts @@ -0,0 +1,133 @@ +/* + * 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 { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; +import { ScaffolderService } from '@backstage/plugin-scaffolder-node'; + +export const createListScaffolderTasksAction = ({ + actionsRegistry, + auth, + scaffolderService, +}: { + actionsRegistry: ActionsRegistryService; + auth: AuthService; + scaffolderService: ScaffolderService; +}) => { + actionsRegistry.register({ + name: 'list-scaffolder-tasks', + title: 'List Scaffolder Tasks', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, + description: ` +This allows you to list scaffolder tasks that have been created. +Each task has a unique id, specification, and status (one of open, processing, completed, failed, cancelled, skipped). +Each task includes a timestamp for when it was created, and an optional last heartbeat timestamp indicating the most recent activity. +Set owned to true to return only tasks created by the current user; omit or set to false for all tasks the credentials can see. +Pagination is supported via limit and offset. + `, + schema: { + input: z => + z.object({ + owned: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, return only tasks created by the current user. Requires a user identity.', + ), + limit: z + .number() + .int() + .min(1) + .max(1000) + .describe('The maximum number of tasks to return for pagination') + .optional(), + offset: z + .number() + .int() + .min(0) + .describe('The offset to start from for pagination') + .optional(), + }), + output: z => + z + .object({ + tasks: z + .array( + z.object({ + id: z.string().describe('The task identifier'), + spec: z.unknown().describe('The task specification'), + status: z + .string() + .describe( + 'Task status: open, processing, completed, failed, cancelled, or skipped', + ), + createdAt: z + .string() + .describe('Timestamp when the task was created'), + lastHeartbeatAt: z + .string() + .optional() + .describe('Timestamp of the last heartbeat'), + }), + ) + .describe('The list of scaffolder tasks'), + totalTasks: z + .number() + .describe('Total number of tasks matching the filter'), + }) + .describe('Object containing a tasks array and totalTasks count'), + }, + action: async ({ input, credentials }) => { + if (input.owned && !auth.isPrincipal(credentials, 'user')) { + throw new NotAllowedError( + 'Filtering by owned tasks requires a user identity.', + ); + } + + const createdBy = + input.owned && auth.isPrincipal(credentials, 'user') + ? credentials.principal.userEntityRef + : undefined; + + const { items, totalItems } = await scaffolderService.listTasks( + { + createdBy, + limit: input.limit, + offset: input.offset, + }, + { credentials }, + ); + + return { + output: { + tasks: items.map(task => ({ + id: task.id, + spec: task.spec, + status: task.status, + createdAt: task.createdAt, + lastHeartbeatAt: task.lastHeartbeatAt, + })), + totalTasks: totalItems, + }, + }; + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 07a25b9e0e..c063e38cb2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -896,6 +896,405 @@ describe('NunjucksWorkflowRunner', () => { expectTaskLog('info: *** {"thing":"***"}'); }); + + // eslint-disable-next-line jest/expect-expect + it('should redact secrets that have been transformed with a replace filter', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.secret); + }, + schema: { + input: { + type: 'object', + required: ['secret'], + properties: { + secret: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + secret: "${{ secrets.backstageToken | replace('.', '_DOT_') }}", + }, + }, + ], + }, + { backstageToken: 'header.payload.signature' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact secrets transformed with the upper filter', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.secret); + }, + schema: { + input: { + type: 'object', + required: ['secret'], + properties: { + secret: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + secret: '${{ secrets.mySecret | upper }}', + }, + }, + ], + }, + { mySecret: 'super-secret-token' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact secrets embedded in a larger string with other text', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.message); + }, + schema: { + input: { + type: 'object', + required: ['message'], + properties: { + message: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + message: + "scaffold-init:${{ secrets.backstageToken | replace('.', '_DOT_') }}", + }, + }, + ], + }, + { backstageToken: 'header.payload.signature' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact environment secrets that have been transformed', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.secret); + }, + schema: { + input: { + type: 'object', + required: ['secret'], + properties: { + secret: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + secret: '${{ environment.secrets.AWS_ACCESS_KEY | upper }}', + }, + }, + ], + }, + {}, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + it('should not redact non-secret values in rendered input', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.name); + }, + schema: { + input: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec({ + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + name: '${{ parameters.serviceName }}', + }, + }, + ], + parameters: { serviceName: 'my-service' }, + }); + + await runner.execute(task); + + expectTaskLog('info: my-service'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact secrets in deeply nested input objects', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.nested.deep.secret); + }, + schema: { + input: { + type: 'object', + required: ['nested'], + properties: { + nested: { + type: 'object', + properties: { + deep: { + type: 'object', + properties: { + secret: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + nested: { + deep: { + secret: "${{ secrets.token | replace('.', '-') }}", + }, + }, + }, + }, + ], + }, + { token: 'aaa.bbb.ccc' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact secrets in arrays within input', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(ctx.input.items[0]); + }, + schema: { + input: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + items: ['${{ secrets.token | upper }}', 'not-a-secret'], + }, + }, + ], + }, + { token: 'my-secret' }, + ); + + await runner.execute(task); + + expectTaskLog('info: ***'); + }); + + // eslint-disable-next-line jest/expect-expect + it('should redact multiple different transformed secrets in the same step', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: async ctx => { + ctx.logger.info(`${ctx.input.a} ${ctx.input.b}`); + }, + schema: { + input: { + type: 'object', + required: ['a', 'b'], + properties: { + a: { type: 'string' }, + b: { type: 'string' }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + a: '${{ secrets.s1 | upper }}', + b: "${{ secrets.s2 | replace('.', '_') }}", + }, + }, + ], + }, + { s1: 'first-secret', s2: 'second.secret' }, + ); + + await runner.execute(task); + + expectTaskLog('info: *** ***'); + }); + + it('should still pass the correct transformed value to the action input', async () => { + actionRegistry.register({ + id: 'log-secret', + description: 'Mock action for testing', + supportsDryRun: true, + handler: fakeActionHandler, + schema: { + input: { + type: 'object', + required: ['secret'], + properties: { + secret: { + type: 'string', + }, + }, + }, + }, + }); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'log-secret', + input: { + secret: "${{ secrets.backstageToken | replace('.', '_DOT_') }}", + }, + }, + ], + }, + { backstageToken: 'header.payload.signature' }, + ); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { secret: 'header_DOT_payload_DOT_signature' }, + }), + ); + }); }); describe('each', () => { @@ -1066,6 +1465,50 @@ describe('NunjucksWorkflowRunner', () => { } }); + it('should run a step repeatedly - only iterations where the "if" condition is truthy', async () => { + const truthyConditions = [true, 1, 'a', {}]; + const falsyConditions = [false, 0, null, '']; + const conditions = [...truthyConditions, ...falsyConditions]; + const task = createMockTaskWithSpec({ + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.conditions}}', + action: 'jest-mock-action', + input: { condition: '${{each.value}}' }, + if: '${{each.value}}', + }, + ], + parameters: { + conditions, + }, + }); + await runner.execute(task); + + truthyConditions.forEach((condition, idx) => { + expectTaskLog( + `info: Running step each: {"key":"${idx}","value":"${condition}"}`, + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { condition } }), + ); + }); + + falsyConditions.forEach((condition, idx) => { + expectTaskLog( + `info: Skipping step each: {"key":"${ + idx + truthyConditions.length + }","value":"${condition}"}`, + ); + expect(fakeActionHandler).not.toHaveBeenCalledWith( + expect.objectContaining({ input: { condition } }), + ); + }); + + expect(fakeActionHandler).toHaveBeenCalledTimes(truthyConditions.length); + }); + it('should run a step repeatedly with validation of single-expression value', async () => { const numbers = [5, 7, 9]; const task = createMockTaskWithSpec({ @@ -1610,6 +2053,42 @@ describe('NunjucksWorkflowRunner', () => { expect(fakeActionHandler.mock.calls[0][0].step.id).toEqual('test'); expect(fakeActionHandler.mock.calls[0][0].step.name).toEqual('name'); }); + + it('should not pass environment secrets or task secrets to action inputs during dry-run', async () => { + const dryRunHandler = jest.fn(); + actionRegistry.register( + createTemplateAction({ + id: 'jest-dryrun-action', + description: 'Mock action with dry-run support', + supportsDryRun: true, + handler: dryRunHandler, + }), + ); + + const task = createMockTaskWithSpec( + { + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-dryrun-action', + input: { + envSecret: '${{ environment.secrets.AWS_ACCESS_KEY }}', + taskSecret: '${{ secrets.mySecret }}', + }, + }, + ], + }, + { mySecret: 'task-secret-value', backstageToken: token }, + true, + ); + + await runner.execute(task); + + const handlerCall = dryRunHandler.mock.calls[0][0]; + expect(handlerCall.input.envSecret).toBeUndefined(); + expect(handlerCall.input.taskSecret).toBeUndefined(); + }); }); describe('permissions', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 3b31450329..589ad2e204 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -129,6 +129,53 @@ const createStepLogger = ({ return { taskLogger }; }; +/** + * Recursively compares two rendered objects and returns string values from + * `withSecrets` that differ from their counterpart in `withoutSecrets`. + * These are values that were influenced by secret interpolation and should + * be added as log redactions. + */ +function collectSecretRedactions( + withSecrets: unknown, + withoutSecrets: unknown, +): string[] { + if (typeof withSecrets === 'string') { + return withSecrets !== withoutSecrets ? [withSecrets] : []; + } + if (Array.isArray(withSecrets)) { + const other = Array.isArray(withoutSecrets) ? withoutSecrets : []; + return withSecrets.flatMap((val, i) => + collectSecretRedactions(val, other[i]), + ); + } + if (withSecrets && typeof withSecrets === 'object') { + const other = + withoutSecrets && typeof withoutSecrets === 'object' + ? (withoutSecrets as Record) + : {}; + return Object.entries(withSecrets as Record).flatMap( + ([key, val]) => collectSecretRedactions(val, other[key]), + ); + } + return []; +} + +/** + * Extracts all string values from a nested object structure. + * Used as a fallback when the comparison render fails. + */ +function extractStringValues(obj: unknown): string[] { + if (typeof obj === 'string') return [obj]; + if (Array.isArray(obj)) return obj.flatMap(extractStringValues); + if (obj && typeof obj === 'object') { + return Object.entries(obj).flatMap(([key, val]) => [ + key, + ...extractStringValues(val), + ]); + } + return []; +} + const isActionAuthorized = createConditionAuthorizer( Object.values(scaffolderActionRules), ); @@ -270,6 +317,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { if ( step.if === false || (typeof step.if === 'string' && + step.each === undefined && !isTruthy(this.render(step.if, context, renderTemplate))) ) { await stepTrack.skipFalsy(); @@ -340,20 +388,18 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } } + const preIterationContext = { + ...context, + environment: { + parameters: this.environment?.parameters ?? {}, + secrets: task.isDryRun ? {} : this.environment?.secrets ?? {}, + }, + secrets: task.isDryRun ? {} : task.secrets ?? {}, + }; + const resolvedEach = step.each && - this.render( - step.each, - { - ...context, - environment: { - parameters: this.environment?.parameters || {}, - secrets: this.environment?.secrets ?? {}, - }, - secrets: task?.secrets ?? {}, - }, - renderTemplate, - ); + this.render(step.each, preIterationContext, renderTemplate); if (step.each && !resolvedEach) { throw new InputError( @@ -367,26 +413,29 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { each: { key, value }, })) : [{}] - ).map(i => ({ - ...i, - // Secrets are only passed when templating the input to actions for security reasons - input: step.input - ? this.render( - step.input, - { - ...context, - environment: { - parameters: this.environment?.parameters ?? {}, - secrets: this.environment?.secrets ?? {}, - }, - secrets: task.secrets ?? {}, - ...i, - }, - renderTemplate, - ) - : {}, - })); + ).map(i => { + const fullContext = { ...preIterationContext, ...i }; + // Evaluate if condition once per iteration, only when using 'each' + const shouldRun = + !('each' in i) || + !step.if || + isTruthy(this.render(step.if, fullContext, renderTemplate)); + + return { + ...i, + shouldRun, + // Secrets are only passed when templating the input to actions for security reasons + input: step.input + ? this.render(step.input, fullContext, renderTemplate) + : {}, + }; + }); for (const iteration of iterations) { + if (!iteration.shouldRun) { + // No need to check schema or authorization for iterations that will not run + continue; + } + const actionId = `${action.id}${ iteration.each ? `[${iteration.each.key}]` : '' }`; @@ -424,15 +473,58 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { for (const iteration of iterations) { if (iteration.each) { + if (!iteration.shouldRun) { + taskLogger.info( + `Skipping step each: ${JSON.stringify( + iteration.each, + (k, v) => (k ? String(v) : v), + 0, + )}`, + ); + continue; + } taskLogger.info( `Running step each: ${JSON.stringify( iteration.each, - (k, v) => (k ? v.toString() : v), + (k, v) => (k ? String(v) : v), 0, )}`, ); } + // Redact any rendered values that were influenced by secrets. + // Re-render the input without secrets and diff against the real render + // to find values that changed due to secret interpolation. + if (step.input) { + const hasSecrets = + Object.keys(task.secrets ?? {}).length > 0 || + Object.keys(this.environment?.secrets ?? {}).length > 0; + + if (hasSecrets) { + try { + const contextNoSecrets = { + ...preIterationContext, + ...(iteration.each ? { each: iteration.each } : {}), + secrets: {}, + environment: { + ...preIterationContext.environment, + secrets: {}, + }, + }; + const inputWithoutSecrets = this.render( + step.input, + contextNoSecrets, + renderTemplate, + ); + taskLogger.addRedactions( + collectSecretRedactions(iteration.input, inputWithoutSecrets), + ); + } catch { + taskLogger.addRedactions(extractStringValues(iteration.input)); + } + } + } + await action.handler({ input: iteration.input, task: { diff --git a/plugins/scaffolder-backend/src/schema/openapi.yaml b/plugins/scaffolder-backend/src/schema/openapi.yaml index b26515a71b..2baf9fbba3 100644 --- a/plugins/scaffolder-backend/src/schema/openapi.yaml +++ b/plugins/scaffolder-backend/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: scaffolder version: '1' @@ -35,7 +35,6 @@ components: name: kind in: path required: true - allowReserved: true schema: type: string limit: @@ -51,14 +50,12 @@ components: name: namespace in: path required: true - allowReserved: true schema: type: string name: name: name in: path required: true - allowReserved: true schema: type: string offset: @@ -94,7 +91,6 @@ components: name: taskId in: path required: true - allowReserved: true schema: type: string requestBodies: {} @@ -224,8 +220,7 @@ components: - type: boolean - type: number - type: string - - type: object - nullable: true + - type: 'null' description: A type representing all allowed JSON primitive values. JsonValue: oneOf: @@ -428,8 +423,9 @@ components: description: type: string value: - type: object - nullable: true + anyOf: + - type: object + - type: 'null' required: - value description: The response shape for a single global value in the `listTemplatingExtensions` call to the `scaffolder-backend` @@ -690,6 +686,19 @@ paths: type: string required: - id + '400': + description: Validation errors. + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + $ref: '#/components/schemas/ValidationError' + required: + - errors security: - {} - JWT: [] @@ -860,13 +869,11 @@ paths: - in: path name: provider required: true - allowReserved: true schema: type: string - in: path name: resource required: true - allowReserved: true schema: type: string diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/apis/Api.server.ts index c5bd9f0ac2..af824321b2 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -116,7 +116,7 @@ export type Retry = { taskId: string; }; body: RetryRequest; - response: Scaffold201Response; + response: Scaffold201Response | Scaffold400Response; }; /** * @public diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/apis/index.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/apis/index.ts index 8d81cbaf39..9a0ffed740 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/index.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/index.ts index dec4b8804e..58fc52487a 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/index.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Action.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Action.model.ts index f9c028f3e3..a29e2f704d 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Action.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Action.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionExample.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionExample.model.ts index 50550e2883..c7a5cf5075 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionExample.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionExample.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionSchema.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionSchema.model.ts index 35d2014181..e8cc85c647 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionSchema.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ActionSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200Response.model.ts index e3eec0aa20..eb59d6a061 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts index 00ccb41b0f..fe329488d5 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete400Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete400Response.model.ts index 1456babb96..3dae0fd0f1 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete400Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Autocomplete400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/AutocompleteRequest.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/AutocompleteRequest.model.ts index 769a929918..197582c944 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/AutocompleteRequest.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/AutocompleteRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/CancelTask200Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/CancelTask200Response.model.ts index f29c509d41..b23fafc892 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/CancelTask200Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/CancelTask200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200Response.model.ts index 62e3689d5d..d0c1450785 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts index 2646ca49f9..ced04f2ae6 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts index 2a462edefa..9818ca581e 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -23,7 +23,6 @@ */ export interface DryRun200ResponseAllOfStepsInner { [key: string]: any; - id: string; name: string; action: string; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequest.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequest.model.ts index 212b36c26b..254605a6b4 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequest.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts index ddcae4fd9d..f01e2d18c8 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResult.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResult.model.ts index 6e2ad23ef4..cfdcf163bd 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResult.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResult.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts index 4c6b786d82..496650d8ba 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts index e3b19be045..a56e7b1107 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonPrimitive.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonPrimitive.model.ts index 6aa6394c14..0142269c5b 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonPrimitive.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonPrimitive.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -22,4 +22,4 @@ * A type representing all allowed JSON primitive values. * @public */ -export type JsonPrimitive = any | boolean | number | string; +export type JsonPrimitive = boolean | number | string; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonValue.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonValue.model.ts index 9fe2ac9de3..a706c3c93e 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonValue.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/JsonValue.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTasksResponse.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTasksResponse.model.ts index 9347c766a9..0f641660fe 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTasksResponse.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTasksResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts index 74b7cd3780..6c65af534c 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts index 1f022a0675..b827a9f083 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/RetryRequest.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/RetryRequest.model.ts index 858fa67566..b25d0a2ed5 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/RetryRequest.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/RetryRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold201Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold201Response.model.ts index 1271642e81..b69aa7b00c 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold201Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold400Response.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold400Response.model.ts index e3cb7eaa45..d42caec5a4 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold400Response.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/Scaffold400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts index 93f4ae71c0..dbcc372253 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts index 3e0f4ef866..7e664963eb 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedFile.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedFile.model.ts index 65a3c714a7..74ee62f05a 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedFile.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedFile.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTask.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTask.model.ts index 67cbf14279..779efcab87 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTask.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTask.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts index 6e4ddaa296..da412a5839 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskEventType.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskEventType.model.ts index d32d77a448..daa28d1d62 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskEventType.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskEventType.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecrets.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecrets.model.ts index c78c39c1ec..0cbaa38a55 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecrets.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecrets.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskStatus.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskStatus.model.ts index 1f209a3656..75df8d3679 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskStatus.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskStatus.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilter.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilter.model.ts index 7c631978e6..3339c61a77 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilter.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilter.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts index 5a95a58ead..930a9760d9 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts index ee19ce723b..7567b3112d 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts index b3a39c3b24..731cf53496 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts index 95d177a05a..d85fe0c395 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts index 8638ee8114..dd71410c86 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -25,7 +25,6 @@ import { TemplateParameterSchemaStepsInner } from '../models/TemplateParameterSc */ export interface TemplateParameterSchema { [key: string]: any; - title: string; description?: string; steps: Array; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts index 2f7892fd41..ef94e58643 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationError.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationError.model.ts index 7e762f616c..43c45c8351 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationError.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -25,7 +25,6 @@ import { ValidationErrorPathInner } from '../models/ValidationErrorPathInner.mod */ export interface ValidationError { [key: string]: any; - path: Array; property: string; message: string; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts index 85c9576fd0..123b2a0d76 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts index 4535c735db..53d3b9d2e7 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/index.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/models/index.ts index 1bc447fa3d..3d5ceab140 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -23,7 +23,6 @@ export * from '../models/Autocomplete400Response.model'; export * from '../models/AutocompleteRequest.model'; export * from '../models/CancelTask200Response.model'; export * from '../models/DryRun200Response.model'; -export * from '../models/DryRun200ResponseAllOf.model'; export * from '../models/DryRun200ResponseAllOfDirectoryContentsInner.model'; export * from '../models/DryRun200ResponseAllOfStepsInner.model'; export * from '../models/DryRunRequest.model'; @@ -31,7 +30,6 @@ export * from '../models/DryRunRequestDirectoryContentsInner.model'; export * from '../models/DryRunResult.model'; export * from '../models/DryRunResultLogInner.model'; export * from '../models/DryRunResultLogInnerBody.model'; -export * from '../models/DryRunResultLogInnerBodyAllOf.model'; export * from '../models/ErrorError.model'; export * from '../models/ErrorRequest.model'; export * from '../models/ErrorResponse.model'; @@ -51,7 +49,6 @@ export * from '../models/SerializedTask.model'; export * from '../models/SerializedTaskEvent.model'; export * from '../models/TaskEventType.model'; export * from '../models/TaskSecrets.model'; -export * from '../models/TaskSecretsAllOf.model'; export * from '../models/TaskStatus.model'; export * from '../models/TemplateFilter.model'; export * from '../models/TemplateFilterSchema.model'; diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/router.ts b/plugins/scaffolder-backend/src/schema/openapi/generated/router.ts index 6401c84964..1f12056a69 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/router.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: 'scaffolder', version: '1', @@ -69,7 +69,6 @@ export const spec = { name: 'kind', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -89,7 +88,6 @@ export const spec = { name: 'namespace', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -98,7 +96,6 @@ export const spec = { name: 'name', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -144,7 +141,6 @@ export const spec = { name: 'taskId', in: 'path', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -325,8 +321,7 @@ export const spec = { type: 'string', }, { - type: 'object', - nullable: true, + type: 'null', }, ], description: 'A type representing all allowed JSON primitive values.', @@ -605,8 +600,14 @@ export const spec = { type: 'string', }, value: { - type: 'object', - nullable: true, + anyOf: [ + { + type: 'object', + }, + { + type: 'null', + }, + ], }, }, required: ['value'], @@ -1022,6 +1023,25 @@ export const spec = { }, }, }, + '400': { + description: 'Validation errors.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + errors: { + type: 'array', + items: { + $ref: '#/components/schemas/ValidationError', + }, + }, + }, + required: ['errors'], + }, + }, + }, + }, }, security: [ {}, @@ -1272,7 +1292,6 @@ export const spec = { in: 'path', name: 'provider', required: true, - allowReserved: true, schema: { type: 'string', }, @@ -1281,7 +1300,6 @@ export const spec = { in: 'path', name: 'resource', required: true, - allowReserved: true, schema: { type: 'string', }, diff --git a/plugins/scaffolder-backend/src/schema/openapi/index.ts b/plugins/scaffolder-backend/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/index.ts +++ b/plugins/scaffolder-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 16a6000224..9da276c67b 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -20,17 +20,7 @@ import express from 'express'; import request from 'supertest'; import ObservableImpl from 'zen-observable'; -/** - * TODO: The following should import directly from the router file. - * Due to a circular dependency between this plugin and the - * plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error: - * TypeError: _pluginscaffolderbackend.createTemplateAction is not a function - */ -import { - parseEntityRef, - stringifyEntityRef, - UserEntity, -} from '@backstage/catalog-model'; +import { stringifyEntityRef, UserEntity } from '@backstage/catalog-model'; import { createTemplateAction, TaskBroker, @@ -89,9 +79,9 @@ function createDatabase(): DatabaseService { const config = new ConfigReader({}); -// todo: this needs to return a new object every time as there seems to -// be some mutation in the tests. -const generateMockTemplate = () => ({ +// Returns a new mock template object each time to avoid mutation issues. +// Accepts optional spec overrides that are merged with the base spec. +const generateMockTemplate = (specOverrides?: Record) => ({ apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { @@ -152,6 +142,7 @@ const generateMockTemplate = () => ({ }, }, ], + ...specOverrides, }, }); @@ -159,7 +150,7 @@ const mockUser: UserEntity = { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { - name: 'guest', + name: 'mock', annotations: { 'google.com/email': 'bobby@tables.com', }, @@ -181,6 +172,7 @@ const createTestRouter = async ( | CreatedTemplateGlobal[]; autocompleteHandlers?: Record; actionsRegistry?: ActionsService; + entities?: any[]; } = {}, ) => { const logger = mockServices.logger.mock({ @@ -202,26 +194,13 @@ const createTestRouter = async ( jest.spyOn(taskBroker, 'vacuumTasks'); jest.spyOn(taskBroker, 'event$'); - const catalog = catalogServiceMock.mock(); + const entities = overrides.entities ?? [generateMockTemplate(), mockUser]; + const catalog = catalogServiceMock({ entities }); const permissions = mockServices.permissions(); const auth = mockServices.auth(); const httpAuth = mockServices.httpAuth(); const events = mockServices.events(); - catalog.getEntityByRef.mockImplementation(async ref => { - const { kind } = parseEntityRef(ref); - - if (kind.toLocaleLowerCase() === 'template') { - return generateMockTemplate(); - } - - if (kind.toLocaleLowerCase() === 'user') { - return mockUser; - } - - throw new Error(`no mock found for kind: ${kind}`); - }); - const router = await createRouter({ logger, config: new ConfigReader({}), @@ -657,6 +636,132 @@ describe('scaffolder router', () => { expect(response.status).toEqual(400); }); + it('rejects when required secrets are missing', async () => { + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + type: 'object', + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const { router } = await createTestRouter({ + entities: [templateWithSecrets, mockUser], + }); + + const response = await request(router) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + requiredParameter1: 'required-value-1', + requiredParameter2: 'required-value-2', + }, + // No secrets provided + }); + + expect(response.status).toEqual(400); + expect(response.body.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + property: 'secrets', + message: 'secrets.NPM_TOKEN is required', + }), + ]), + ); + }); + + it('rejects when required secrets are missing without explicit type', async () => { + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + // No explicit type: 'object' - should still work + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const { router } = await createTestRouter({ + entities: [templateWithSecrets, mockUser], + }); + + const response = await request(router) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + requiredParameter1: 'required-value-1', + requiredParameter2: 'required-value-2', + }, + // No secrets provided + }); + + expect(response.status).toEqual(400); + expect(response.body.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + property: 'secrets', + message: 'secrets.NPM_TOKEN is required', + }), + ]), + ); + }); + + it('accepts valid secrets matching the schema', async () => { + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + type: 'object', + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const { router, taskBroker } = await createTestRouter({ + entities: [templateWithSecrets, mockUser], + }); + const broker = taskBroker.dispatch as jest.Mocked['dispatch']; + + broker.mockResolvedValue({ + taskId: 'a-random-id', + }); + + const response = await request(router) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + requiredParameter1: 'required-value-1', + requiredParameter2: 'required-value-2', + }, + secrets: { + NPM_TOKEN: 'my-secret-token', + }, + }); + + expect(response.status).toEqual(201); + expect(response.body.id).toBe('a-random-id'); + }); + it('return the template id', async () => { const { router, taskBroker } = await createTestRouter(); const broker = taskBroker.dispatch as jest.Mocked['dispatch']; @@ -1093,6 +1198,97 @@ describe('scaffolder router', () => { }); }); + describe('POST /v2/tasks/:taskId/retry', () => { + it('rejects when required secrets are missing', async () => { + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + type: 'object', + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const { router, taskBroker } = await createTestRouter({ + entities: [templateWithSecrets, mockUser], + }); + + (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ + id: 'a-random-id', + spec: { + templateInfo: { + entityRef: 'template:default/create-react-app-template', + baseUrl: 'https://example.com', + entity: { metadata: templateWithSecrets.metadata }, + }, + } as any, + status: 'failed', + createdAt: '', + createdBy: 'user:default/mock', + }); + + const response = await request(router) + .post('/v2/tasks/a-random-id/retry') + .send({}); + + expect(response.status).toEqual(400); + expect(response.body.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + property: 'secrets', + message: 'secrets.NPM_TOKEN is required', + }), + ]), + ); + }); + + it('accepts valid secrets on retry', async () => { + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + type: 'object', + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const { router, taskBroker } = await createTestRouter({ + entities: [templateWithSecrets, mockUser], + }); + + (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ + id: 'a-random-id', + spec: { + templateInfo: { + entityRef: 'template:default/create-react-app-template', + baseUrl: 'https://example.com', + entity: { metadata: templateWithSecrets.metadata }, + }, + } as any, + status: 'failed', + createdAt: '', + createdBy: 'user:default/mock', + }); + + const response = await request(router) + .post('/v2/tasks/a-random-id/retry') + .send({ + secrets: { + NPM_TOKEN: 'my-secret-token', + }, + }); + + expect(response.status).toEqual(201); + expect(taskBroker.retry).toHaveBeenCalled(); + }); + }); + describe('GET /v2/tasks/:taskId/eventstream', () => { it('should return log messages', async () => { const { unwrappedRouter: router, taskBroker } = await createTestRouter(); @@ -1413,6 +1609,9 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ const mockToken = mockCredentials.user.token(); const mockTemplate = generateMockTemplate(); + // Spy on the catalog method to verify it's called correctly + const getEntityByRefSpy = jest.spyOn(catalog, 'getEntityByRef'); + await request(router) .post('/v2/dry-run') .set('Authorization', `Bearer ${mockToken}`) @@ -1425,13 +1624,54 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ directoryContents: [], }); - expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(getEntityByRefSpy).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByRef).toHaveBeenCalledWith( + expect(getEntityByRefSpy).toHaveBeenCalledWith( 'user:default/mock', expect.anything(), ); }); + + it('rejects when required secrets are missing', async () => { + const { router } = await createTestRouter(); + const mockToken = mockCredentials.user.token(); + + const templateWithSecrets = generateMockTemplate({ + secrets: { + schema: { + type: 'object', + required: ['NPM_TOKEN'], + properties: { + NPM_TOKEN: { type: 'string' }, + }, + }, + }, + }); + + const response = await request(router) + .post('/v2/dry-run') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + template: templateWithSecrets, + values: { + requiredParameter1: 'required-value-1', + requiredParameter2: 'required-value-2', + }, + directoryContents: [], + // No secrets provided + }); + + expect(response.status).toEqual(400); + expect(response.body.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + property: 'secrets', + message: 'secrets.NPM_TOKEN is required', + }), + ]), + ); + }); + it('allows payloads up to 10MB', async () => { const { unwrappedRouter } = await createTestRouter(); const mockToken = mockCredentials.user.token(); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index ef2a39eb54..0c9daf53c2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -16,6 +16,7 @@ import { AuditorService, + AuditorServiceEvent, AuthService, BackstageCredentials, DatabaseService, @@ -26,7 +27,7 @@ import { resolveSafeChildPath, SchedulerService, } from '@backstage/backend-plugin-api'; -import { validate } from 'jsonschema'; +import { validate, ValidatorResult } from 'jsonschema'; import { CompoundEntityRef, Entity, @@ -181,6 +182,49 @@ const readDuration = ( return defaultValue; }; +function formatSecretsValidationErrors(result: ValidatorResult) { + return result.errors.map(err => { + const property = err.property.replace(/^instance/, 'secrets'); + const secretName = err.argument; + const message = + err.name === 'required' + ? `secrets.${secretName} is required` + : `${property} ${err.message}`; + return { + ...err, + property, + message, + instance: {}, + }; + }); +} + +async function validateSecrets(options: { + template: TemplateEntityV1beta3; + secrets: Record; + res: express.Response; + auditorEvent?: AuditorServiceEvent; +}): Promise { + const { template, secrets, res, auditorEvent } = options; + if (!template.spec.secrets?.schema) { + return true; + } + + const result = validate(secrets, template.spec.secrets.schema); + if (result.valid) { + return true; + } + + await auditorEvent?.fail({ + error: new InputError('Secrets validation failed'), + }); + + res.status(400).json({ + errors: formatSecretsValidationErrors(result), + }); + return false; +} + /** * A method to create a router for the scaffolder backend plugin. */ @@ -527,6 +571,16 @@ export async function createRouter( } } + const secretsValid = await validateSecrets({ + template, + secrets: req.body.secrets ?? {}, + res, + auditorEvent, + }); + if (!secretsValid) { + return; + } + const baseUrl = getEntityBaseUrl(template); const taskSpec: TaskSpec = { @@ -747,6 +801,28 @@ export async function createRouter( isTaskAuthorized, }); + // Validate secrets against template schema if defined + if (task.spec.templateInfo?.entityRef) { + const templateEntityRef = parseEntityRef( + task.spec.templateInfo.entityRef, + { defaultKind: 'template' }, + ); + const template = await authorizeTemplate( + templateEntityRef, + credentials, + ); + + const secretsValid = await validateSecrets({ + template, + secrets: req.body.secrets ?? {}, + res, + auditorEvent, + }); + if (!secretsValid) { + return; + } + } + await auditorEvent?.success(); const { token } = await auth.getPluginRequestToken({ @@ -975,6 +1051,16 @@ export async function createRouter( } } + const secretsValid = await validateSecrets({ + template, + secrets: body.secrets ?? {}, + res, + auditorEvent, + }); + if (!secretsValid) { + return; + } + const steps = template.spec.steps.map((step, index) => ({ ...step, id: step.id ?? `step-${index + 1}`, diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 3c524f9833..8320424432 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-scaffolder-common +## 2.0.0-next.2 + +### Minor Changes + +- e8736ea: Added an optional `secrets` field to `TemplateEntityV1beta3` for configuring secrets validation. The schema for validating secrets is defined under `secrets.schema` as a JSON Schema object. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.2 + +## 2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated `bitbucket` integration from being registered in the `ScaffolderClient`. Use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Minor Changes + +- f598909: **BREAKING PRODUCERS**: Made `retry`, `listTasks`, `listTemplatingExtensions`, `dryRun`, and `autocomplete` required methods on the `ScaffolderApi` interface. Implementations of `ScaffolderApi` must now provide these methods. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## 1.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## 1.7.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-permission-common@0.9.6 + ## 1.7.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 2ba975aef4..571dbe57a7 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.7.6-next.1", + "version": "2.0.0-next.2", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-common/report.api.md b/plugins/scaffolder-common/report.api.md index c6c5141cf2..5b82272955 100644 --- a/plugins/scaffolder-common/report.api.md +++ b/plugins/scaffolder-common/report.api.md @@ -58,7 +58,7 @@ export type LogEvent = { // @public export interface ScaffolderApi { // (undocumented) - autocomplete?( + autocomplete( request: { token: string; provider: string; @@ -79,99 +79,6 @@ export interface ScaffolderApi { status?: ScaffolderTaskStatus; }>; // (undocumented) - dryRun?( - request: ScaffolderDryRunOptions, - options?: ScaffolderRequestOptions, - ): Promise; - // (undocumented) - getIntegrationsList( - options: ScaffolderGetIntegrationsListOptions, - ): Promise; - // (undocumented) - getTask( - taskId: string, - options?: ScaffolderRequestOptions, - ): Promise; - // (undocumented) - getTemplateParameterSchema( - templateRef: string, - options?: ScaffolderRequestOptions, - ): Promise; - listActions(options?: ScaffolderRequestOptions): Promise; - // (undocumented) - listTasks?( - request: { - filterByOwnership: 'owned' | 'all'; - limit?: number; - offset?: number; - }, - options?: ScaffolderRequestOptions, - ): Promise<{ - tasks: ScaffolderTask[]; - totalTasks?: number; - }>; - listTemplatingExtensions?( - options?: ScaffolderRequestOptions, - ): Promise; - retry?( - taskId: string, - options?: ScaffolderRequestOptions, - ): Promise<{ - id: string; - }>; - scaffold( - request: ScaffolderScaffoldOptions, - options?: ScaffolderRequestOptions, - ): Promise; - // (undocumented) - streamLogs( - request: ScaffolderStreamLogsOptions, - options?: ScaffolderRequestOptions, - ): Observable; -} - -// @public -export class ScaffolderClient implements ScaffolderApi { - constructor(options: { - discoveryApi: { - getBaseUrl(pluginId: string): Promise; - }; - fetchApi: { - fetch: typeof fetch; - }; - identityApi?: { - getBackstageIdentity(): Promise<{ - type: 'user'; - userEntityRef: string; - ownershipEntityRefs: string[]; - }>; - }; - scmIntegrationsApi: ScmIntegrationRegistry; - useLongPollingLogs?: boolean; - }); - autocomplete({ - token, - resource, - provider, - context, - }: { - token: string; - provider: string; - resource: string; - context: Record; - }): Promise<{ - results: { - title?: string; - id: string; - }[]; - }>; - cancelTask( - taskId: string, - options?: ScaffolderRequestOptions, - ): Promise<{ - status?: ScaffolderTaskStatus; - }>; - // (undocumented) dryRun( request: ScaffolderDryRunOptions, options?: ScaffolderRequestOptions, @@ -206,7 +113,98 @@ export class ScaffolderClient implements ScaffolderApi { listTemplatingExtensions( options?: ScaffolderRequestOptions, ): Promise; - retry?( + retry( + taskId: string, + options?: ScaffolderRequestOptions, + ): Promise<{ + id: string; + }>; + scaffold( + request: ScaffolderScaffoldOptions, + options?: ScaffolderRequestOptions, + ): Promise; + // (undocumented) + streamLogs( + request: ScaffolderStreamLogsOptions, + options?: ScaffolderRequestOptions, + ): Observable; +} + +// @public +export class ScaffolderClient implements ScaffolderApi { + constructor(options: { + discoveryApi: { + getBaseUrl(pluginId: string): Promise; + }; + fetchApi: { + fetch: typeof fetch; + }; + identityApi?: { + getBackstageIdentity(): Promise<{ + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; + }>; + }; + scmIntegrationsApi: ScmIntegrationRegistry; + useLongPollingLogs?: boolean; + }); + autocomplete( + input: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options?: ScaffolderRequestOptions, + ): Promise<{ + results: { + title?: string; + id: string; + }[]; + }>; + cancelTask( + taskId: string, + options?: ScaffolderRequestOptions, + ): Promise<{ + status?: ScaffolderTaskStatus; + }>; + // (undocumented) + dryRun( + request: ScaffolderDryRunOptions, + options?: ScaffolderRequestOptions, + ): Promise; + // (undocumented) + getIntegrationsList( + options: ScaffolderGetIntegrationsListOptions, + ): Promise; + // (undocumented) + getTask( + taskId: string, + options?: ScaffolderRequestOptions, + ): Promise; + // (undocumented) + getTemplateParameterSchema( + templateRef: string, + options?: ScaffolderRequestOptions, + ): Promise; + listActions(options?: ScaffolderRequestOptions): Promise; + // (undocumented) + listTasks( + request: { + filterByOwnership: 'owned' | 'all'; + limit?: number; + offset?: number; + }, + options?: ScaffolderRequestOptions, + ): Promise<{ + tasks: ScaffolderTask[]; + totalTasks?: number; + }>; + listTemplatingExtensions( + options?: ScaffolderRequestOptions, + ): Promise; + retry( taskId: string, options?: ScaffolderRequestOptions, ): Promise<{ @@ -420,6 +418,9 @@ export interface TemplateEntityV1beta3 extends Entity { input?: JsonObject; }[]; parameters?: TemplateParametersV1beta3 | TemplateParametersV1beta3[]; + secrets?: { + schema?: JsonObject; + }; steps: Array; output?: { [name: string]: string; diff --git a/plugins/scaffolder-common/src/ScaffolderClient.ts b/plugins/scaffolder-common/src/ScaffolderClient.ts index d40f7a4637..3aaa7ebefc 100644 --- a/plugins/scaffolder-common/src/ScaffolderClient.ts +++ b/plugins/scaffolder-common/src/ScaffolderClient.ts @@ -125,13 +125,6 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise { const integrations = [ ...this.scmIntegrationsApi.azure.list(), - ...this.scmIntegrationsApi.bitbucket - .list() - .filter( - item => - !this.scmIntegrationsApi.bitbucketCloud.byHost(item.config.host) && - !this.scmIntegrationsApi.bitbucketServer.byHost(item.config.host), - ), ...this.scmIntegrationsApi.bitbucketCloud.list(), ...this.scmIntegrationsApi.bitbucketServer.list(), ...this.scmIntegrationsApi.gerrit.list(), @@ -381,7 +374,7 @@ export class ScaffolderClient implements ScaffolderApi { /** * {@inheritdoc ScaffolderApi.retry} */ - async retry?( + async retry( taskId: string, options?: ScaffolderRequestOptions, ): Promise<{ id: string }> { @@ -393,22 +386,28 @@ export class ScaffolderClient implements ScaffolderApi { /** * {@inheritdoc ScaffolderApi.retry} */ - async autocomplete({ - token, - resource, - provider, - context, - }: { - token: string; - provider: string; - resource: string; - context: Record; - }): Promise<{ results: { title?: string; id: string }[] }> { + async autocomplete( + { + token, + resource, + provider, + context, + }: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options?: ScaffolderRequestOptions, + ): Promise<{ results: { title?: string; id: string }[] }> { return await this.requestRequired( - await this.apiClient.autocomplete({ - path: { provider, resource }, - body: { token, context }, - }), + await this.apiClient.autocomplete( + { + path: { provider, resource }, + body: { token, context }, + }, + options, + ), ); } diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index a9f846068c..5eb61a0f6d 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -146,6 +146,16 @@ } ] }, + "secrets": { + "type": "object", + "description": "Configuration for secrets that are passed during task creation.", + "properties": { + "schema": { + "type": "object", + "description": "A JSONSchema for validating secrets passed during task creation." + } + } + }, "presentation": { "type": "object", "description": "A way to redefine the presentation of the scaffolder.", diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts index 1851b7e70f..d48049946c 100644 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts +++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts @@ -67,6 +67,13 @@ export interface TemplateEntityV1beta3 extends Entity { * variables passed from the user into each action in the template. */ parameters?: TemplateParametersV1beta3 | TemplateParametersV1beta3[]; + /** + * Configuration for secrets that are passed during task creation. + * The schema field contains a JSONSchema used to validate secrets. + */ + secrets?: { + schema?: JsonObject; + }; /** * A list of steps to be executed in sequence which are defined by the template. These steps are a list of the underlying * javascript action and some optional input parameters that may or may not have been collected from the end user. diff --git a/plugins/scaffolder-common/src/api.ts b/plugins/scaffolder-common/src/api.ts index 5425607d34..5e61af5bb4 100644 --- a/plugins/scaffolder-common/src/api.ts +++ b/plugins/scaffolder-common/src/api.ts @@ -285,12 +285,12 @@ export interface ScaffolderApi { * * @param taskId - the id of the task */ - retry?( + retry( taskId: string, options?: ScaffolderRequestOptions, ): Promise<{ id: string }>; - listTasks?( + listTasks( request: { filterByOwnership: 'owned' | 'all'; limit?: number; @@ -311,7 +311,7 @@ export interface ScaffolderApi { /** * Returns a structure describing the available templating extensions. */ - listTemplatingExtensions?( + listTemplatingExtensions( options?: ScaffolderRequestOptions, ): Promise; @@ -320,12 +320,12 @@ export interface ScaffolderApi { options?: ScaffolderRequestOptions, ): Observable; - dryRun?( + dryRun( request: ScaffolderDryRunOptions, options?: ScaffolderRequestOptions, ): Promise; - autocomplete?( + autocomplete( request: { token: string; provider: string; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/apis/Api.client.ts b/plugins/scaffolder-common/src/schema/openapi/generated/apis/Api.client.ts index ca5ae10b8f..e87c838d1a 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/apis/Api.client.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -17,6 +17,7 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** + import { DiscoveryApi } from '../types/discovery'; import { FetchApi } from '../types/fetch'; import crossFetch from 'cross-fetch'; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/apis/index.ts b/plugins/scaffolder-common/src/schema/openapi/generated/apis/index.ts index fc7c83b736..c2e931caf8 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/apis/index.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/index.ts b/plugins/scaffolder-common/src/schema/openapi/generated/index.ts index dc3055033d..19501a5dcb 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/index.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Action.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Action.model.ts index f9c028f3e3..a29e2f704d 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Action.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Action.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionExample.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionExample.model.ts index 50550e2883..c7a5cf5075 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionExample.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionExample.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionSchema.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionSchema.model.ts index 35d2014181..e8cc85c647 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionSchema.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ActionSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200Response.model.ts index e3eec0aa20..eb59d6a061 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts index 00ccb41b0f..fe329488d5 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete200ResponseResultsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete400Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete400Response.model.ts index 1456babb96..3dae0fd0f1 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete400Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Autocomplete400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/AutocompleteRequest.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/AutocompleteRequest.model.ts index 769a929918..197582c944 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/AutocompleteRequest.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/AutocompleteRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/CancelTask200Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/CancelTask200Response.model.ts index f29c509d41..b23fafc892 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/CancelTask200Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/CancelTask200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200Response.model.ts index 62e3689d5d..d0c1450785 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts index 2646ca49f9..ced04f2ae6 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfDirectoryContentsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts index 2a462edefa..9818ca581e 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOfStepsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -23,7 +23,6 @@ */ export interface DryRun200ResponseAllOfStepsInner { [key: string]: any; - id: string; name: string; action: string; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequest.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequest.model.ts index 212b36c26b..254605a6b4 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequest.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts index ddcae4fd9d..f01e2d18c8 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunRequestDirectoryContentsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResult.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResult.model.ts index 6e2ad23ef4..cfdcf163bd 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResult.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResult.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts index 4c6b786d82..496650d8ba 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts index e3b19be045..a56e7b1107 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBody.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonPrimitive.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonPrimitive.model.ts index 6aa6394c14..0142269c5b 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonPrimitive.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonPrimitive.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -22,4 +22,4 @@ * A type representing all allowed JSON primitive values. * @public */ -export type JsonPrimitive = any | boolean | number | string; +export type JsonPrimitive = boolean | number | string; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonValue.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonValue.model.ts index 9fe2ac9de3..a706c3c93e 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonValue.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/JsonValue.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTasksResponse.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTasksResponse.model.ts index 9347c766a9..0f641660fe 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTasksResponse.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTasksResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts index 74b7cd3780..6c65af534c 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts index 1f022a0675..b827a9f083 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ListTemplatingExtensionsResponseGlobals.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/RetryRequest.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/RetryRequest.model.ts index 858fa67566..b25d0a2ed5 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/RetryRequest.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/RetryRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold201Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold201Response.model.ts index 1271642e81..b69aa7b00c 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold201Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold400Response.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold400Response.model.ts index e3cb7eaa45..d42caec5a4 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold400Response.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/Scaffold400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts index 93f4ae71c0..dbcc372253 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderScaffoldOptions.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts index 3e0f4ef866..7e664963eb 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ScaffolderUsageExample.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedFile.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedFile.model.ts index 65a3c714a7..74ee62f05a 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedFile.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedFile.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTask.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTask.model.ts index 67cbf14279..779efcab87 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTask.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTask.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts index 6e4ddaa296..da412a5839 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/SerializedTaskEvent.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskEventType.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskEventType.model.ts index d32d77a448..daa28d1d62 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskEventType.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskEventType.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecrets.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecrets.model.ts index c78c39c1ec..0cbaa38a55 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecrets.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskSecrets.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskStatus.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskStatus.model.ts index 1f209a3656..75df8d3679 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskStatus.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TaskStatus.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilter.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilter.model.ts index 7c631978e6..3339c61a77 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilter.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilter.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts index 5a95a58ead..930a9760d9 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateFilterSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts index ee19ce723b..7567b3112d 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunction.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts index b3a39c3b24..731cf53496 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalFunctionSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts index 95d177a05a..d85fe0c395 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateGlobalValue.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts index 8638ee8114..dd71410c86 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchema.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -25,7 +25,6 @@ import { TemplateParameterSchemaStepsInner } from '../models/TemplateParameterSc */ export interface TemplateParameterSchema { [key: string]: any; - title: string; description?: string; steps: Array; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts index 2f7892fd41..ef94e58643 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/TemplateParameterSchemaStepsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationError.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationError.model.ts index 7e762f616c..43c45c8351 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationError.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -25,7 +25,6 @@ import { ValidationErrorPathInner } from '../models/ValidationErrorPathInner.mod */ export interface ValidationError { [key: string]: any; - path: Array; property: string; message: string; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts index 85c9576fd0..123b2a0d76 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorArgument.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts index 4535c735db..53d3b9d2e7 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/ValidationErrorPathInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/index.ts b/plugins/scaffolder-common/src/schema/openapi/generated/models/index.ts index 1bc447fa3d..3d5ceab140 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/index.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -23,7 +23,6 @@ export * from '../models/Autocomplete400Response.model'; export * from '../models/AutocompleteRequest.model'; export * from '../models/CancelTask200Response.model'; export * from '../models/DryRun200Response.model'; -export * from '../models/DryRun200ResponseAllOf.model'; export * from '../models/DryRun200ResponseAllOfDirectoryContentsInner.model'; export * from '../models/DryRun200ResponseAllOfStepsInner.model'; export * from '../models/DryRunRequest.model'; @@ -31,7 +30,6 @@ export * from '../models/DryRunRequestDirectoryContentsInner.model'; export * from '../models/DryRunResult.model'; export * from '../models/DryRunResultLogInner.model'; export * from '../models/DryRunResultLogInnerBody.model'; -export * from '../models/DryRunResultLogInnerBodyAllOf.model'; export * from '../models/ErrorError.model'; export * from '../models/ErrorRequest.model'; export * from '../models/ErrorResponse.model'; @@ -51,7 +49,6 @@ export * from '../models/SerializedTask.model'; export * from '../models/SerializedTaskEvent.model'; export * from '../models/TaskEventType.model'; export * from '../models/TaskSecrets.model'; -export * from '../models/TaskSecretsAllOf.model'; export * from '../models/TaskStatus.model'; export * from '../models/TemplateFilter.model'; export * from '../models/TemplateFilterSchema.model'; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/pluginId.ts b/plugins/scaffolder-common/src/schema/openapi/generated/pluginId.ts index b898028be5..fcfaf1d7c0 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/pluginId.ts +++ b/plugins/scaffolder-common/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-common/src/schema/openapi/index.ts b/plugins/scaffolder-common/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/plugins/scaffolder-common/src/schema/openapi/index.ts +++ b/plugins/scaffolder-common/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index 5cb017f63b..796c4e8999 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.2 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + +## 0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## 0.3.8 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/backend-test-utils@1.11.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-scaffolder-node@0.12.5 + ## 0.3.8-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index 94df70fc60..a6eb1fe410 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.3.8-next.1", + "version": "0.3.9-next.2", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 6ce2b0ad10..c050736dc1 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-scaffolder-node +## 0.13.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.2 + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + +## 0.13.0-next.1 + +### Minor Changes + +- e27bd4e: **BREAKING** Removed deprecated `bitbucket` integration from being used in the `parseRepoUrl` function. It will use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Patch Changes + +- f598909: Added `scaffolderServiceRef` and `ScaffolderService` interface for backend plugins that need to interact with the scaffolder API using `BackstageCredentials` instead of raw tokens. +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## 0.12.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## 0.12.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 4fc7bf0: Bump to tar v7 +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 0.12.5-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 009ee9e509..a79c47d367 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.12.5-next.1", + "version": "0.13.0-next.2", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", @@ -27,6 +27,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha/index.ts", + "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -36,6 +37,9 @@ "alpha": [ "src/alpha/index.ts" ], + "testUtils": [ + "src/testUtils.ts" + ], "package.json": [ "package.json" ] @@ -79,6 +83,15 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", - "@types/lodash": "^4.14.151" + "@types/lodash": "^4.14.151", + "msw": "^1.0.0" + }, + "peerDependencies": { + "@backstage/backend-test-utils": "workspace:^" + }, + "peerDependenciesMeta": { + "@backstage/backend-test-utils": { + "optional": true + } } } diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index e1d49f571a..3a01b7f788 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -11,11 +11,7 @@ import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder import { z } from 'zod'; // @alpha -export type AutocompleteHandler = ({ - resource, - token, - context, -}: { +export type AutocompleteHandler = (input: { resource: string; token: string; context: Record; @@ -125,10 +121,7 @@ export const restoreWorkspace: (opts: { // @alpha export interface ScaffolderAutocompleteExtensionPoint { // (undocumented) - addAutocompleteProvider({ - id, - handler, - }: { + addAutocompleteProvider(input: { id: string; handler: AutocompleteHandler; }): void; @@ -217,13 +210,7 @@ export interface WorkspaceProvider { targetPath: string; }): Promise; // (undocumented) - serializeWorkspace({ - path, - taskId, - }: { - path: string; - taskId: string; - }): Promise; + serializeWorkspace(input: { path: string; taskId: string }): Promise; } // @alpha (undocumented) diff --git a/plugins/scaffolder-node/report-testUtils.api.md b/plugins/scaffolder-node/report-testUtils.api.md new file mode 100644 index 0000000000..e4f9c55ee2 --- /dev/null +++ b/plugins/scaffolder-node/report-testUtils.api.md @@ -0,0 +1,122 @@ +## API Report File for "@backstage/plugin-scaffolder-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { ListActionsResponse } from '@backstage/plugin-scaffolder-common'; +import { ListTemplatingExtensionsResponse } from '@backstage/plugin-scaffolder-common'; +import { LogEvent } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderDryRunOptions } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderDryRunResponse } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderScaffoldOptions } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderScaffoldResponse } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderTask } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-common'; +import { ServiceMock } from '@backstage/backend-test-utils'; +import type { TemplateParameterSchema } from '@backstage/plugin-scaffolder-common'; + +// @public +export interface ScaffolderService { + // (undocumented) + autocomplete( + request: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + results: { + title?: string; + id: string; + }[]; + }>; + // (undocumented) + cancelTask( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + status?: ScaffolderTaskStatus; + }>; + // (undocumented) + dryRun( + request: ScaffolderDryRunOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getLogs( + request: { + taskId: string; + after?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getTask( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getTemplateParameterSchema( + request: { + templateRef: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + listActions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + listTasks( + request: { + createdBy?: string; + limit?: number; + offset?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + items: ScaffolderTask[]; + totalItems: number; + }>; + // (undocumented) + listTemplatingExtensions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + retry( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + id: string; + }>; + // (undocumented) + scaffold( + request: ScaffolderScaffoldOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; +} + +// @public +export namespace scaffolderServiceMock { + const mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; +} + +// @public (undocumented) +export interface ScaffolderServiceRequestOptions { + // (undocumented) + credentials: BackstageCredentials; +} +``` diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index cbba30dac7..24b51ed989 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -9,15 +9,26 @@ import { Expand } from '@backstage/types'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; +import { ListActionsResponse } from '@backstage/plugin-scaffolder-common'; +import { ListTemplatingExtensionsResponse } from '@backstage/plugin-scaffolder-common'; +import { LogEvent } from '@backstage/plugin-scaffolder-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Observable } from '@backstage/types'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { ScaffolderDryRunOptions } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderDryRunResponse } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderScaffoldOptions } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderScaffoldResponse } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderTask } from '@backstage/plugin-scaffolder-common'; +import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { SpawnOptionsWithoutStdio } from 'node:child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import type { TemplateParameterSchema } from '@backstage/plugin-scaffolder-common'; import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; @@ -307,6 +318,21 @@ export const parseRepoUrl: ( project?: string; }; +// @public (undocumented) +export function removeFiles(options: { + dir: string; + filepath: string; + auth: + | { + username: string; + password: string; + } + | { + token: string; + }; + logger?: LoggerService | undefined; +}): Promise; + // @public export interface ScaffolderActionsExtensionPoint { // (undocumented) @@ -316,6 +342,110 @@ export interface ScaffolderActionsExtensionPoint { // @public export const scaffolderActionsExtensionPoint: ExtensionPoint; +// @public +export interface ScaffolderService { + // (undocumented) + autocomplete( + request: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + results: { + title?: string; + id: string; + }[]; + }>; + // (undocumented) + cancelTask( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + status?: ScaffolderTaskStatus; + }>; + // (undocumented) + dryRun( + request: ScaffolderDryRunOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getLogs( + request: { + taskId: string; + after?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getTask( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + getTemplateParameterSchema( + request: { + templateRef: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + listActions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + listTasks( + request: { + createdBy?: string; + limit?: number; + offset?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + items: ScaffolderTask[]; + totalItems: number; + }>; + // (undocumented) + listTemplatingExtensions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + // (undocumented) + retry( + request: { + taskId: string; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ + id: string; + }>; + // (undocumented) + scaffold( + request: ScaffolderScaffoldOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; +} + +// @public +export const scaffolderServiceRef: ServiceRef< + ScaffolderService, + 'plugin', + 'singleton' +>; + +// @public (undocumented) +export interface ScaffolderServiceRequestOptions { + // (undocumented) + credentials: BackstageCredentials; +} + // @public (undocumented) export interface SerializedFile { // (undocumented) diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts index d70ddf3bc0..96a8b96242 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.test.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.test.ts @@ -17,6 +17,7 @@ import { Git } from '../scm'; import { addFiles, + removeFiles, cloneRepo, commitAndPushBranch, commitAndPushRepo, @@ -31,6 +32,7 @@ jest.mock('../scm', () => ({ fromAuth: jest.fn().mockReturnValue({ init: jest.fn(), add: jest.fn(), + remove: jest.fn(), checkout: jest.fn(), branch: jest.fn(), commit: jest @@ -528,6 +530,47 @@ describe('addFiles', () => { }); }); +describe('removeFiles', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with minimal parameters', () => { + beforeEach(async () => { + await removeFiles({ + dir: '/tmp/repo/dir/', + filepath: 'file-to-remove.txt', + auth: { + username: 'test-user', + password: 'test-password', + }, + }); + }); + + it('removes the file', () => { + expect(mockedGit.remove).toHaveBeenCalledWith({ + filepath: 'file-to-remove.txt', + dir: '/tmp/repo/dir/', + }); + }); + }); + + it('with token', async () => { + await removeFiles({ + dir: '/tmp/repo/dir/', + filepath: 'file-to-remove.txt', + auth: { + token: 'test-token', + }, + }); + + expect(mockedGit.remove).toHaveBeenCalledWith({ + filepath: 'file-to-remove.txt', + dir: '/tmp/repo/dir/', + }); + }); +}); + describe('commitAndPushBranch', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/plugins/scaffolder-node/src/actions/gitHelpers.ts b/plugins/scaffolder-node/src/actions/gitHelpers.ts index 49650de92e..8ae4ec6a4f 100644 --- a/plugins/scaffolder-node/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-node/src/actions/gitHelpers.ts @@ -204,6 +204,27 @@ export async function addFiles(options: { await git.add({ dir, filepath }); } +/** + * @public + */ +export async function removeFiles(options: { + dir: string; + filepath: string; + // For use cases where token has to be used with Basic Auth + // it has to be provided as password together with a username + // which may be a fixed value defined by the provider. + auth: { username: string; password: string } | { token: string }; + logger?: LoggerService | undefined; +}): Promise { + const { dir, filepath, auth, logger } = options; + const git = Git.fromAuth({ + ...auth, + logger, + }); + + await git.remove({ dir, filepath }); +} + /** * @public */ diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index 686d89d844..47939f34cc 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -30,6 +30,7 @@ export { commitAndPushRepo, commitAndPushBranch, addFiles, + removeFiles, createBranch, cloneRepo, } from './gitHelpers'; diff --git a/plugins/scaffolder-node/src/actions/util.test.ts b/plugins/scaffolder-node/src/actions/util.test.ts index 792e306d00..8afbbf553a 100644 --- a/plugins/scaffolder-node/src/actions/util.test.ts +++ b/plugins/scaffolder-node/src/actions/util.test.ts @@ -54,11 +54,11 @@ describe('scaffolder action utils', () => { /No matching integration configuration for host/, ); }); - describe('bitbucket', () => { - beforeEach(() => byHost.mockReturnValue({ type: 'bitbucket' })); + describe('bitbucketCloud', () => { + beforeEach(() => byHost.mockReturnValue({ type: 'bitbucketCloud' })); describe('cloud', () => { const [host, workspace, project, repo] = [ - 'www.bitbucket.org', + 'bitbucket.org', 'foo', 'bar', 'baz', @@ -97,6 +97,9 @@ describe('scaffolder action utils', () => { repo, })); }); + }); + describe('bitbucketServer', () => { + beforeEach(() => byHost.mockReturnValue({ type: 'bitbucketServer' })); describe('other', () => { const [host, project, repo] = ['bitbucket.other', 'foo', 'bar']; it('requires project', () => diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 0e90122cdd..e698306a45 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -84,10 +84,11 @@ export const parseRepoUrl = ( ]), ); switch (type) { - case 'bitbucket': { - if (host === 'www.bitbucket.org') { - checkRequiredParams(parsed, 'workspace'); - } + case 'bitbucketCloud': { + checkRequiredParams(parsed, 'workspace', 'project', 'repo'); + break; + } + case 'bitbucketServer': { checkRequiredParams(parsed, 'project', 'repo'); break; } diff --git a/plugins/scaffolder-node/src/index.ts b/plugins/scaffolder-node/src/index.ts index a9aa77ba4e..5a518c8628 100644 --- a/plugins/scaffolder-node/src/index.ts +++ b/plugins/scaffolder-node/src/index.ts @@ -25,3 +25,4 @@ export * from './tasks'; export * from './files'; export * from './types'; export * from './extensions'; +export * from './scaffolderService'; diff --git a/plugins/scaffolder-node/src/scaffolderService.test.ts b/plugins/scaffolder-node/src/scaffolderService.test.ts new file mode 100644 index 0000000000..de0870751f --- /dev/null +++ b/plugins/scaffolder-node/src/scaffolderService.test.ts @@ -0,0 +1,205 @@ +/* + * 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 { + createBackendModule, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { + ServiceFactoryTester, + mockCredentials, + mockServices, + registerMswTestHooks, + startTestBackend, +} from '@backstage/backend-test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { scaffolderServiceRef } from './scaffolderService'; + +describe('scaffolderServiceRef', () => { + const server = setupServer(); + registerMswTestHooks(server); + + it('should return a scaffolder service', async () => { + expect.assertions(1); + const testModule = createBackendModule({ + moduleId: 'test', + pluginId: 'test', + register(env) { + env.registerInit({ + deps: { + scaffolder: scaffolderServiceRef, + }, + async init({ scaffolder }) { + expect(scaffolder.getTask).toBeDefined(); + }, + }); + }, + }); + + await startTestBackend({ + features: [testModule], + }); + }); + + it('should inject token from user credentials', async () => { + expect.assertions(1); + + server.use( + rest.get('*/api/scaffolder/v2/tasks/:taskId', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'scaffolder', + }), + ); + return res( + ctx.json({ + id: 'task-1', + spec: {}, + status: 'completed', + createdAt: '2025-01-01T00:00:00Z', + }), + ); + }), + ); + + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const scaffolder = await tester.getService(scaffolderServiceRef); + + await scaffolder.getTask( + { taskId: 'task-1' }, + { + credentials: mockCredentials.user(), + }, + ); + }); + + it('should inject token from service credentials', async () => { + expect.assertions(1); + + server.use( + rest.get('*/api/scaffolder/v2/tasks/:taskId', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.service(), + targetPluginId: 'scaffolder', + }), + ); + return res( + ctx.json({ + id: 'task-1', + spec: {}, + status: 'completed', + createdAt: '2025-01-01T00:00:00Z', + }), + ); + }), + ); + + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const scaffolder = await tester.getService(scaffolderServiceRef); + + await scaffolder.getTask( + { taskId: 'task-1' }, + { + credentials: mockCredentials.service(), + }, + ); + }); + + it('should pass credentials for direct HTTP calls like getLogs', async () => { + expect.assertions(1); + + server.use( + rest.get('*/api/scaffolder/v2/tasks/:taskId/events', (req, res, ctx) => { + expect(req.headers.get('authorization')).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'scaffolder', + }), + ); + return res(ctx.json([])); + }), + ); + + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const scaffolder = await tester.getService(scaffolderServiceRef); + + await scaffolder.getLogs( + { taskId: 'task-1' }, + { credentials: mockCredentials.user() }, + ); + }); + + it('should pass createdBy and pagination params for listTasks', async () => { + expect.assertions(4); + + server.use( + rest.get('*/api/scaffolder/v2/tasks', (req, res, ctx) => { + expect(req.url.searchParams.get('createdBy')).toBe( + 'user:default/guest', + ); + expect(req.url.searchParams.get('limit')).toBe('10'); + expect(req.url.searchParams.get('offset')).toBe('5'); + return res(ctx.json({ tasks: [], totalTasks: 0 })); + }), + ); + + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'unused-dummy' }), + deps: {}, + factory() {}, + }), + { dependencies: [mockServices.discovery.factory()] }, + ); + + const scaffolder = await tester.getService(scaffolderServiceRef); + + const result = await scaffolder.listTasks( + { createdBy: 'user:default/guest', limit: 10, offset: 5 }, + { credentials: mockCredentials.user() }, + ); + + expect(result).toEqual({ items: [], totalItems: 0 }); + }); +}); diff --git a/plugins/scaffolder-node/src/scaffolderService.ts b/plugins/scaffolder-node/src/scaffolderService.ts new file mode 100644 index 0000000000..b978164c7f --- /dev/null +++ b/plugins/scaffolder-node/src/scaffolderService.ts @@ -0,0 +1,338 @@ +/* + * 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 { + AuthService, + BackstageCredentials, + coreServices, + createServiceFactory, + createServiceRef, + DiscoveryService, +} from '@backstage/backend-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import { + ListActionsResponse, + ListTemplatingExtensionsResponse, + LogEvent, + ScaffolderClient, + ScaffolderDryRunOptions, + ScaffolderDryRunResponse, + ScaffolderRequestOptions, + ScaffolderScaffoldOptions, + ScaffolderScaffoldResponse, + ScaffolderTask, + ScaffolderTaskStatus, +} from '@backstage/plugin-scaffolder-common'; +import type { TemplateParameterSchema } from '@backstage/plugin-scaffolder-common'; + +/** + * @public + */ +export interface ScaffolderServiceRequestOptions { + credentials: BackstageCredentials; +} + +/** + * A backend service interface for the scaffolder that uses + * {@link @backstage/backend-plugin-api#BackstageCredentials} instead of tokens. + * + * @public + */ +export interface ScaffolderService { + getTemplateParameterSchema( + request: { templateRef: string }, + options: ScaffolderServiceRequestOptions, + ): Promise; + + scaffold( + request: ScaffolderScaffoldOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; + + getTask( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise; + + cancelTask( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ status?: ScaffolderTaskStatus }>; + + retry( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ id: string }>; + + listTasks( + request: { + createdBy?: string; + limit?: number; + offset?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ items: ScaffolderTask[]; totalItems: number }>; + + listActions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + + listTemplatingExtensions( + request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise; + + getLogs( + request: { + taskId: string; + after?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise; + + dryRun( + request: ScaffolderDryRunOptions, + options: ScaffolderServiceRequestOptions, + ): Promise; + + autocomplete( + request: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ results: { title?: string; id: string }[] }>; +} + +class DefaultScaffolderService implements ScaffolderService { + readonly #auth: AuthService; + readonly #client: ScaffolderClient; + readonly #discovery: DiscoveryService; + + constructor(options: { + auth: AuthService; + client: ScaffolderClient; + discovery: DiscoveryService; + }) { + this.#auth = options.auth; + this.#client = options.client; + this.#discovery = options.discovery; + } + + async getTemplateParameterSchema( + request: { templateRef: string }, + options: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.getTemplateParameterSchema( + request.templateRef, + await this.#getOptions(options), + ); + } + + async scaffold( + request: ScaffolderScaffoldOptions, + options: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.scaffold(request, await this.#getOptions(options)); + } + + async getTask( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.getTask( + request.taskId, + await this.#getOptions(options), + ); + } + + async cancelTask( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ status?: ScaffolderTaskStatus }> { + return this.#client.cancelTask( + request.taskId, + await this.#getOptions(options), + ); + } + + async retry( + request: { taskId: string }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ id: string }> { + return this.#client.retry(request.taskId, await this.#getOptions(options)); + } + + async listTasks( + request: { + createdBy?: string; + limit?: number; + offset?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ items: ScaffolderTask[]; totalItems: number }> { + const { token } = await this.#getOptions(options); + const baseUrl = await this.#discovery.getBaseUrl('scaffolder'); + + const params = new URLSearchParams(); + if (request.createdBy) { + params.set('createdBy', request.createdBy); + } + if (request.limit !== undefined) { + params.set('limit', String(request.limit)); + } + if (request.offset !== undefined) { + params.set('offset', String(request.offset)); + } + + const query = params.toString(); + const url = `${baseUrl}/v2/tasks${query ? `?${query}` : ''}`; + + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + const body = await response.json(); + return { + items: body.tasks, + totalItems: body.totalTasks ?? 0, + }; + } + + async listActions( + _request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.listActions( + options ? await this.#getOptions(options) : {}, + ); + } + + async listTemplatingExtensions( + _request?: {}, + options?: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.listTemplatingExtensions( + options ? await this.#getOptions(options) : {}, + ); + } + + async getLogs( + request: { + taskId: string; + after?: number; + }, + options: ScaffolderServiceRequestOptions, + ): Promise { + const { token } = await this.#getOptions(options); + const baseUrl = await this.#discovery.getBaseUrl('scaffolder'); + + const params = new URLSearchParams(); + if (request.after !== undefined) { + params.set('after', String(request.after)); + } + + const query = params.toString(); + const taskId = encodeURIComponent(request.taskId); + const url = `${baseUrl}/v2/tasks/${taskId}/events${ + query ? `?${query}` : '' + }`; + + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json(); + } + + async dryRun( + request: ScaffolderDryRunOptions, + options: ScaffolderServiceRequestOptions, + ): Promise { + return this.#client.dryRun(request, await this.#getOptions(options)); + } + + async autocomplete( + request: { + token: string; + provider: string; + resource: string; + context: Record; + }, + options: ScaffolderServiceRequestOptions, + ): Promise<{ results: { title?: string; id: string }[] }> { + return this.#client.autocomplete(request, await this.#getOptions(options)); + } + + async #getOptions( + options: ScaffolderServiceRequestOptions, + ): Promise { + return this.#auth.getPluginRequestToken({ + onBehalfOf: options.credentials, + targetPluginId: 'scaffolder', + }); + } +} + +/** + * A service ref for the scaffolder client, to be used by backend plugins + * and modules that need to interact with the scaffolder API. + * + * @public + */ +export const scaffolderServiceRef = createServiceRef({ + id: 'scaffolder-client', + defaultFactory: async service => + createServiceFactory({ + service, + deps: { + auth: coreServices.auth, + discovery: coreServices.discovery, + config: coreServices.rootConfig, + }, + async factory({ auth, discovery, config }) { + const integrations = ScmIntegrations.fromConfig(config); + const client = new ScaffolderClient({ + discoveryApi: discovery, + fetchApi: { fetch }, + scmIntegrationsApi: integrations, + }); + return new DefaultScaffolderService({ + auth, + client, + discovery, + }); + }, + }), +}); diff --git a/plugins/scaffolder-node/src/testUtils.ts b/plugins/scaffolder-node/src/testUtils.ts new file mode 100644 index 0000000000..7b1c7fb61d --- /dev/null +++ b/plugins/scaffolder-node/src/testUtils.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Backend test helpers for the Scaffolder plugin. + * + * @packageDocumentation + */ + +export type { + ScaffolderService, + ScaffolderServiceRequestOptions, +} from './scaffolderService'; +export { scaffolderServiceMock } from './testUtils/scaffolderServiceMock'; diff --git a/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.test.ts b/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.test.ts new file mode 100644 index 0000000000..7145d12e2a --- /dev/null +++ b/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { scaffolderServiceMock } from './scaffolderServiceMock'; + +describe('scaffolderServiceMock', () => { + it('creates a mock with all methods as jest.fn()', () => { + const mock = scaffolderServiceMock.mock(); + + expect(mock.getTask).toHaveBeenCalledTimes(0); + }); + + it('supports overriding individual methods', async () => { + const mock = scaffolderServiceMock.mock({ + getTask: jest.fn().mockResolvedValue({ id: 'task-1' }), + }); + + await expect( + mock.getTask({ taskId: 'task-1' }, { credentials: expect.anything() }), + ).resolves.toEqual(expect.objectContaining({ id: 'task-1' })); + }); +}); diff --git a/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.ts b/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.ts new file mode 100644 index 0000000000..7b18c01298 --- /dev/null +++ b/plugins/scaffolder-node/src/testUtils/scaffolderServiceMock.ts @@ -0,0 +1,46 @@ +/* + * 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 { createServiceMock } from '@backstage/backend-test-utils'; +import { scaffolderServiceRef, ScaffolderService } from '../scaffolderService'; + +/** + * A collection of mock functionality for the scaffolder service. + * + * @public + */ +export namespace scaffolderServiceMock { + /** + * Creates a scaffolder service whose methods are mock functions, possibly with + * some of them overloaded by the caller. + */ + export const mock = createServiceMock( + scaffolderServiceRef, + () => ({ + getTemplateParameterSchema: jest.fn(), + scaffold: jest.fn(), + getTask: jest.fn(), + cancelTask: jest.fn(), + retry: jest.fn(), + listTasks: jest.fn(), + listActions: jest.fn(), + listTemplatingExtensions: jest.fn(), + getLogs: jest.fn(), + dryRun: jest.fn(), + autocomplete: jest.fn(), + }), + ); +} diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 0e9f5b8501..44e4a0e08c 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-scaffolder-react +## 1.20.0-next.2 + +### Minor Changes + +- 470f72d: The `LogViewer` component from `@backstage/core-components` now supports downloading logs if a callback is passed to `onDownloadLogs` + +### Patch Changes + +- bd31ddd: Updated dependency `flatted` to `3.3.4`. +- Updated dependencies + - @backstage/frontend-test-utils@0.5.1-next.2 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + +## 1.19.8-next.1 + +### Patch Changes + +- 004b5c1: Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` as alpha exports. +- f598909: Added `scaffolderApiMock` test utility, exported from `@backstage/plugin-scaffolder-react/testUtils`. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 1.19.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## 1.19.7 + +### Patch Changes + +- 2eeca03: Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the following alpha exports were removed: + + - `formFieldsApi` + - `formFieldsApiRef` + - `ScaffolderFormFieldsApi` + +- b9d90a7: Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 1.19.7-next.2 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index e0ae9a6c7a..f48b242f7b 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.7-next.2", + "version": "1.20.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", @@ -31,6 +31,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", + "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -40,6 +41,9 @@ "alpha": [ "src/alpha.ts" ], + "testUtils": [ + "src/testUtils.ts" + ], "package.json": [ "package.json" ] @@ -81,7 +85,7 @@ "ajv": "^8.0.1", "ajv-errors": "^3.0.0", "classnames": "^2.2.6", - "flatted": "3.3.3", + "flatted": "^3.3.4", "humanize-duration": "^3.25.1", "immer": "^9.0.6", "json-schema": "^0.4.0", @@ -115,12 +119,17 @@ "swr": "^2.0.0" }, "peerDependencies": { + "@backstage/frontend-test-utils": "workspace:^", "@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": "^6.30.3", "react-router-dom": "^6.30.2" }, "peerDependenciesMeta": { + "@backstage/frontend-test-utils": { + "optional": true + }, "@types/react": { "optional": true } diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 1b6fb99838..c546f265be 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { AnyApiRef } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiRef } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; @@ -198,6 +199,9 @@ export type FormFieldExtensionData< schema?: FieldSchema, z.output>; }; +// @alpha (undocumented) +export const formFieldsApiRef: ApiRef; + // @alpha (undocumented) export type FormValidation = { [name: string]: FieldValidation | FormValidation; @@ -272,6 +276,12 @@ export type ScaffolderFormDecoratorContext< ) => void; }; +// @alpha (undocumented) +export interface ScaffolderFormFieldsApi { + // (undocumented) + loadFormFields(): Promise; +} + // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, diff --git a/plugins/scaffolder-react/report-testUtils.api.md b/plugins/scaffolder-react/report-testUtils.api.md new file mode 100644 index 0000000000..c33675d93d --- /dev/null +++ b/plugins/scaffolder-react/report-testUtils.api.md @@ -0,0 +1,15 @@ +## API Report File for "@backstage/plugin-scaffolder-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ApiMock } from '@backstage/frontend-test-utils'; +import { ScaffolderApi } from '@backstage/plugin-scaffolder-common'; + +// @public +export namespace scaffolderApiMock { + const mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; +} +``` diff --git a/plugins/scaffolder-react/src/next/api/index.ts b/plugins/scaffolder-react/src/next/api/index.ts index 9988f4dd82..1ecddeaf41 100644 --- a/plugins/scaffolder-react/src/next/api/index.ts +++ b/plugins/scaffolder-react/src/next/api/index.ts @@ -14,4 +14,12 @@ * limitations under the License. */ -export { type FormField } from './types'; +import { createApiRef } from '@backstage/frontend-plugin-api'; +import { ScaffolderFormFieldsApi } from './types'; + +export { type FormField, type ScaffolderFormFieldsApi } from './types'; + +/** @alpha */ +export const formFieldsApiRef = createApiRef({ + id: 'plugin.scaffolder.form-fields-loader', +}); diff --git a/plugins/scaffolder-react/src/next/api/types.ts b/plugins/scaffolder-react/src/next/api/types.ts index 38929ee189..e3ab855708 100644 --- a/plugins/scaffolder-react/src/next/api/types.ts +++ b/plugins/scaffolder-react/src/next/api/types.ts @@ -18,3 +18,8 @@ export interface FormField { readonly $$type: '@backstage/scaffolder/FormField'; } + +/** @alpha */ +export interface ScaffolderFormFieldsApi { + loadFormFields(): Promise; +} diff --git a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.test.tsx b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.test.tsx index 4466abdbd9..f2063068f1 100644 --- a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.test.tsx @@ -15,6 +15,7 @@ */ import { renderInTestApp } from '@backstage/test-utils'; import { ReactNode } from 'react'; +import userEvent from '@testing-library/user-event'; import { TaskLogStream } from './TaskLogStream'; // The inside needs mocking to render in jsdom @@ -25,6 +26,22 @@ jest.mock('react-virtualized-auto-sizer', () => ({ }) => <>{props.children({ width: 400, height: 200 })}, })); +beforeAll(() => { + Reflect.defineProperty(window.URL, 'createObjectURL', { + writable: true, + value: jest.fn((_blob: any) => 'blob:mock-url'), + }); + Reflect.defineProperty(window.URL, 'revokeObjectURL', { + writable: true, + value: jest.fn(), + }); +}); + +afterAll(() => { + Reflect.deleteProperty(window.URL, 'createObjectURL'); + Reflect.deleteProperty(window.URL, 'revokeObjectURL'); +}); + describe('TaskLogStream', () => { it('should render a log stream with the correct log lines', async () => { const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] }; @@ -47,4 +64,99 @@ describe('TaskLogStream', () => { await expect(findAllByRole('row')).resolves.toHaveLength(2); }); + + it('should render download button', async () => { + const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] }; + + const { getByRole } = await renderInTestApp(); + + const downloadButton = getByRole('button', { name: /download/i }); + expect(downloadButton).toBeInTheDocument(); + }); + + it('should download logs when download button is clicked', async () => { + const logs = { + step1: ['line 1', 'line 2'], + step2: ['line 3', 'line 4'], + }; + + const { getByRole } = await renderInTestApp(); + + // Mock only the anchor element creation + const mockAnchor = document.createElement('a'); + const clickSpy = jest.spyOn(mockAnchor, 'click'); + const removeSpy = jest.spyOn(mockAnchor, 'remove'); + + const originalCreateElement = document.createElement.bind(document); + const createElementSpy = jest + .spyOn(document, 'createElement') + .mockImplementation((tagName: string) => { + if (tagName === 'a') { + return mockAnchor; + } + return originalCreateElement(tagName); + }); + + const downloadButton = getByRole('button', { name: /download/i }); + await userEvent.click(downloadButton); + + // Verify file download was triggered + expect(createElementSpy).toHaveBeenCalledWith('a'); + expect(clickSpy).toHaveBeenCalled(); + expect(removeSpy).toHaveBeenCalled(); + + // Verify the download filename contains .log extension + expect(mockAnchor.download).toMatch(/\.log$/); + + // Verify href was set + expect(mockAnchor.href).toContain('blob:'); + + // Restore mocks + createElementSpy.mockRestore(); + }); + + it('should create blob with correct log content when downloading', async () => { + const logs = { + step1: ['line 1', 'line 2'], + step2: ['line 3'], + }; + + const { getByRole } = await renderInTestApp(); + + let capturedBlob: Blob | null = null; + const createObjectURLSpy = jest + .spyOn(URL, 'createObjectURL') + .mockImplementation((blob: any) => { + capturedBlob = blob; + return 'blob:mock-url'; + }); + + const mockAnchor = document.createElement('a'); + const clickSpy = jest.spyOn(mockAnchor, 'click'); + + const originalCreateElement = document.createElement.bind(document); + const createElementSpy = jest + .spyOn(document, 'createElement') + .mockImplementation((tagName: string) => { + if (tagName === 'a') { + return mockAnchor; + } + return originalCreateElement(tagName); + }); + + const downloadButton = getByRole('button', { name: /download/i }); + await userEvent.click(downloadButton); + + // Verify file download was triggered + expect(createElementSpy).toHaveBeenCalledWith('a'); + expect(clickSpy).toHaveBeenCalled(); + + // Verify blob was created with correct content + expect(capturedBlob).toBeInstanceOf(Blob); + expect(capturedBlob!?.type).toBe('text/plain'); + + // Restore mocks + createElementSpy.mockRestore(); + createObjectURLSpy.mockRestore(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx index b88d96510e..e8396167fc 100644 --- a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx +++ b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx @@ -15,6 +15,7 @@ */ import { LogViewer } from '@backstage/core-components'; import { makeStyles } from '@material-ui/core/styles'; +import { useDownloadLogs } from '../../hooks/useDownloadLogs'; const useStyles = makeStyles({ root: { @@ -32,9 +33,13 @@ const useStyles = makeStyles({ */ export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => { const styles = useStyles(); + + const onDownloadLogs = useDownloadLogs(props.logs); + return (
    l.join('\n')) .filter(Boolean) diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index b3dc78dba5..96ca2217b9 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -37,6 +37,9 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), listTasks: jest.fn(), autocomplete: jest.fn(), + retry: jest.fn(), + listTemplatingExtensions: jest.fn(), + dryRun: jest.fn(), }; const catalogApi = catalogApiMock.mock(); diff --git a/plugins/scaffolder-react/src/next/hooks/useDownloadLogs.ts b/plugins/scaffolder-react/src/next/hooks/useDownloadLogs.ts new file mode 100644 index 0000000000..1399e2f285 --- /dev/null +++ b/plugins/scaffolder-react/src/next/hooks/useDownloadLogs.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2026 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 { useCallback } from 'react'; +import { useParams } from 'react-router'; + +export const useDownloadLogs = (logs: { [k: string]: string[] }) => { + const { taskId } = useParams<{ taskId: string }>(); + return useCallback(() => { + const element = document.createElement('a'); + const file = new Blob( + [ + Object.values(logs) + .map(l => l.join('\n')) + .filter(Boolean) + .join('\n'), + ], + { type: 'text/plain' }, + ); + element.href = URL.createObjectURL(file); + element.download = `${taskId}.log`; + element.click(); + URL.revokeObjectURL(element.href); + element.remove(); + }, [logs, taskId]); +}; diff --git a/packages/cli-node/src/paths.ts b/plugins/scaffolder-react/src/testUtils.ts similarity index 74% rename from packages/cli-node/src/paths.ts rename to plugins/scaffolder-react/src/testUtils.ts index 2c658c27b3..bc94b5447e 100644 --- a/packages/cli-node/src/paths.ts +++ b/plugins/scaffolder-react/src/testUtils.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,7 +14,10 @@ * limitations under the License. */ -import { findPaths } from '@backstage/cli-common'; +/** + * Frontend test helpers for the Scaffolder plugin. + * + * @packageDocumentation + */ -/* eslint-disable-next-line no-restricted-syntax */ -export const paths = findPaths(__dirname); +export { scaffolderApiMock } from './testUtils/scaffolderApiMock'; diff --git a/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.test.ts b/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.test.ts new file mode 100644 index 0000000000..4d2ae09d35 --- /dev/null +++ b/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { scaffolderApiMock } from './scaffolderApiMock'; + +describe('scaffolderApiMock', () => { + it('creates a mock with all methods as jest.fn()', () => { + const mock = scaffolderApiMock.mock(); + + expect(mock.getTask).toHaveBeenCalledTimes(0); + expect(mock.scaffold).toHaveBeenCalledTimes(0); + expect(mock.listActions).toHaveBeenCalledTimes(0); + }); + + it('supports overriding individual methods', async () => { + const mock = scaffolderApiMock.mock({ + getTask: jest.fn().mockResolvedValue({ id: 'task-1' }), + }); + + await expect(mock.getTask('task-1')).resolves.toEqual( + expect.objectContaining({ id: 'task-1' }), + ); + }); +}); diff --git a/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.ts b/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.ts new file mode 100644 index 0000000000..5103301079 --- /dev/null +++ b/plugins/scaffolder-react/src/testUtils/scaffolderApiMock.ts @@ -0,0 +1,44 @@ +/* + * 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 { createApiMock } from '@backstage/frontend-test-utils'; +import { scaffolderApiRef } from '../api/ref'; + +/** + * A collection of mock functionality for the scaffolder API. + * + * @public + */ +export namespace scaffolderApiMock { + /** + * Creates a scaffolder API whose methods are mock functions, possibly with + * some of them overloaded by the caller. + */ + export const mock = createApiMock(scaffolderApiRef, () => ({ + getTemplateParameterSchema: jest.fn(), + scaffold: jest.fn(), + getTask: jest.fn(), + cancelTask: jest.fn(), + retry: jest.fn(), + listTasks: jest.fn(), + getIntegrationsList: jest.fn(), + listActions: jest.fn(), + listTemplatingExtensions: jest.fn(), + streamLogs: jest.fn(), + dryRun: jest.fn(), + autocomplete: jest.fn(), + })); +} diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index cb7d2d4fe1..d7a62c2969 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/plugin-scaffolder +## 1.35.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-scaffolder-react@1.20.0-next.2 + - @backstage/plugin-scaffolder-common@2.0.0-next.2 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 1.35.5-next.1 + +### Patch Changes + +- e27bd4e: Removed check for deprecated `bitbucket` integration from `repoPickerValidation` function used by the `RepoUrlPicker`, it now validates the `bitbucketServer` and `bitbucketCloud` integrations instead. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.35.5-next.0 + +### Patch Changes + +- bd5b842: Added a new `ui:autoSelect` option to the EntityPicker field that controls whether an entity is automatically selected when the field loses focus. When set to `false`, the field will remain empty if the user closes it without explicitly selecting an entity, preventing unintentional selections. Defaults to `true` for backward compatibility. +- ee87720: Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.35.3 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 4e581a6: Updated the browser tab title on the template wizard page to display the specific template title instead of the generic "Create a new component" text. +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 2eeca03: Scaffolder form fields in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `FormFieldBlueprint` now uses this new approach, and while form fields created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the following alpha exports were removed: + + - `formFieldsApiRef` + - `ScaffolderFormFieldsApi` + +- b9d90a7: Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-scaffolder-react@1.19.7 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-permission-react@0.4.40 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.6 + ## 1.35.3-next.2 ### Patch Changes diff --git a/plugins/scaffolder/README.md b/plugins/scaffolder/README.md index 0a79d8afc0..9c9ca15dfa 100644 --- a/plugins/scaffolder/README.md +++ b/plugins/scaffolder/README.md @@ -15,13 +15,72 @@ To check if you already have the package, look under `@backstage/plugin-scaffolder`. The instructions below walk through restoring the plugin, if you previously removed it. -### Install the package - ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-scaffolder ``` +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). + +### Troubleshooting + +If you encounter [issues with early closure of the `EventStream`](https://github.com/backstage/backstage/issues/5535) +used to auto-update logs during task execution, you can work around them by enabling +long polling. To do so, update your `packages/app/src/apis.ts` file to register a +`ScaffolderClient` with `useLongPollingLogs` set to `true`. By default, it is `false`. + +```typescript +import { + AnyApiFactory, + createApiFactory, + discoveryApiRef, + fetchApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { + scaffolderApiRef, + ScaffolderClient, +} from '@backstage/plugin-scaffolder'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ scmIntegrationsApi, discoveryApi, identityApi, fetchApi }) => + new ScaffolderClient({ + discoveryApi, + identityApi, + scmIntegrationsApi, + fetchApi, + useLongPollingLogs: true, + }), + }), + // ... other factories +]; +``` + +This replaces the default implementation of the `scaffolderApiRef`. + +### Local development + +When you develop a new template, action or new ``, then we recommend +to launch the plugin locally using the `createDevApp` of the `./dev/index.tsx` file for testing/Debugging purposes + +To play with it, open a terminal and run the command: `yarn start` within the `./plugins/scaffolder` folder + +**NOTE:** Don't forget to open a second terminal, start your Backstage backend there, +and configure the template locations that you want to test. + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + ### Add the plugin to your `packages/app` Add the root page that the scaffolder plugin provides to your app. You can @@ -78,57 +137,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( ``` -### Troubleshooting - -If you encounter the [issue of closing EventStream](https://github.com/backstage/backstage/issues/5535) -which auto-updates logs during task execution, you can enable long polling. To do so, -update your `packages/app/src/apis.ts` file to register a `ScaffolderClient` with the -`useLongPollingLogs` set to `true`. By default, it is `false`. - -```typescript -import { - createApiFactory, - discoveryApiRef, - fetchApiRef, - identityApiRef, -} from '@backstage/core-plugin-api'; -import { - scaffolderApiRef, - ScaffolderClient, -} from '@backstage/plugin-scaffolder'; - -export const apis: AnyApiFactory[] = [ - createApiFactory({ - api: scaffolderApiRef, - deps: { - discoveryApi: discoveryApiRef, - identityApi: identityApiRef, - scmIntegrationsApi: scmIntegrationsApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ scmIntegrationsApi, discoveryApi, identityApi, fetchApi }) => - new ScaffolderClient({ - discoveryApi, - identityApi, - scmIntegrationsApi, - fetchApi, - useLongPollingLogs: true, - }), - }), - // ... other factories -``` - -This replaces the default implementation of the `scaffolderApiRef`. - -### Local development - -When you develop a new template, action or new ``, then we recommend -to launch the plugin locally using the `createDevApp` of the `./dev/index.tsx` file for testing/Debugging purposes - -To play with it, open a terminal and run the command: `yarn start` within the `./plugins/scaffolder` folder - -**NOTE:** Don't forget to open a second terminal and to launch the backend or [backend-next](../../docs/backend-system/index.md) there, using `yarn start` and to specify the locations of the templates to play with ! - ## Links - [scaffolder-backend](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 763fa4db87..b2ee8a47d4 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.35.3-next.2", + "version": "1.35.5-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 0eda5c0ca9..7c96a74c8b 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -17,9 +17,11 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { FilterPredicate } from '@backstage/filter-predicates'; import { FormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { formFieldsApiRef } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_2 } from '@rjsf/core'; import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; @@ -30,6 +32,7 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; +import { ScaffolderFormFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; import { SubRouteRef } from '@backstage/core-plugin-api'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; @@ -194,21 +197,67 @@ const _default: OverridableFrontendPlugin< 'page:scaffolder': OverridableExtensionDefinition<{ config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; formFields: ExtensionInput< ConfigurableExtensionDataRef< () => Promise, @@ -225,10 +274,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; 'scaffolder-form-field:scaffolder/entity-name-picker': OverridableExtensionDefinition<{ @@ -430,6 +481,8 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{ // @alpha (undocumented) export const formDecoratorsApiRef: ApiRef; +export { formFieldsApiRef }; + // @alpha @deprecated export type FormProps = Pick< FormProps_2, @@ -449,6 +502,8 @@ export interface ScaffolderFormDecoratorsApi { getFormDecorators(): Promise; } +export { ScaffolderFormFieldsApi }; + // @public (undocumented) export type ScaffolderTemplateEditorClassKey = | 'root' @@ -562,6 +617,8 @@ export const scaffolderTranslationRef: TranslationRef< 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'; @@ -569,8 +626,6 @@ export const scaffolderTranslationRef: TranslationRef< 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.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.functions.title': 'Functions'; readonly 'templatingExtensions.content.functions.schema.output': 'Output'; readonly 'templatingExtensions.content.functions.schema.arguments': 'Arguments'; diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index e624b157fa..5c0fffbcc1 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -74,6 +74,7 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent_2< export const EntityPickerFieldExtension: FieldExtensionComponent_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -103,6 +104,7 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2< export const EntityPickerFieldSchema: FieldSchema_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -249,6 +251,7 @@ export type MyGroupsPickerUiOptions = NonNullable< export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: @@ -278,6 +281,7 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< export const OwnedEntityPickerFieldSchema: FieldSchema_2< string, { + autoSelect?: boolean | undefined; defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; catalogFilter?: diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.test.tsx index 016d775c43..883aab8754 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.test.tsx @@ -38,6 +38,9 @@ describe('TemplateEditorToolbar', () => { listActions: jest.fn(), listTasks: jest.fn(), autocomplete: jest.fn(), + retry: jest.fn(), + listTemplatingExtensions: jest.fn(), + dryRun: jest.fn(), }; scaffolderApiMock.listActions.mockResolvedValue([ diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx index 6836259dd8..b8d750c419 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -54,6 +54,9 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), listTasks: jest.fn(), autocomplete: jest.fn(), + retry: jest.fn(), + listTemplatingExtensions: jest.fn(), + dryRun: jest.fn(), }; const scaffolderDecoratorsMock: jest.Mocked = { diff --git a/plugins/scaffolder/src/alpha/extensions.tsx b/plugins/scaffolder/src/alpha/extensions.tsx index 8c17f8cc1c..91306bf5fd 100644 --- a/plugins/scaffolder/src/alpha/extensions.tsx +++ b/plugins/scaffolder/src/alpha/extensions.tsx @@ -25,11 +25,13 @@ import { } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from '../routes'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; +import { + FormFieldBlueprint, + formFieldsApiRef, +} from '@backstage/plugin-scaffolder-react/alpha'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { ScaffolderClient } from '../api'; -import { formFieldsApiRef } from './formFieldsApi'; export const scaffolderPage = PageBlueprint.makeWithOverrides({ inputs: { diff --git a/plugins/scaffolder/src/alpha/formFieldsApi.ts b/plugins/scaffolder/src/alpha/formFieldsApi.ts index df88645eb4..b065af9a82 100644 --- a/plugins/scaffolder/src/alpha/formFieldsApi.ts +++ b/plugins/scaffolder/src/alpha/formFieldsApi.ts @@ -16,24 +16,14 @@ import { ApiBlueprint, - createApiRef, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; +import { + FormFieldBlueprint, + formFieldsApiRef, +} from '@backstage/plugin-scaffolder-react/alpha'; import { OpaqueFormField } from '@internal/scaffolder'; -interface FormField { - readonly $$type: '@backstage/scaffolder/FormField'; -} - -interface ScaffolderFormFieldsApi { - loadFormFields(): Promise; -} - -const formFieldsApiRef = createApiRef({ - id: 'plugin.scaffolder.form-fields-loader', -}); - export const formFieldsApi = ApiBlueprint.makeWithOverrides({ name: 'form-fields', inputs: { @@ -68,4 +58,7 @@ export const formFieldsApi = ApiBlueprint.makeWithOverrides({ }, }); -export { formFieldsApiRef }; +export { + formFieldsApiRef, + type ScaffolderFormFieldsApi, +} from '@backstage/plugin-scaffolder-react/alpha'; diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index 53e2b8f0bf..a0b30fd3df 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -24,5 +24,9 @@ export { export { scaffolderTranslationRef } from '../translation'; export * from './api'; +export { + formFieldsApiRef, + type ScaffolderFormFieldsApi, +} from './formFieldsApi'; export { default } from './plugin'; diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 9b7698250a..0c105f3a7c 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -15,6 +15,7 @@ */ import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import { actionsRouteRef, editRouteRef, @@ -59,6 +60,8 @@ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'scaffolder', + title: 'Create', + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index a86b44e724..201073e8a9 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -34,6 +34,9 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), listTasks: jest.fn(), autocomplete: jest.fn(), + retry: jest.fn(), + listTemplatingExtensions: jest.fn(), + dryRun: jest.fn(), }; const mockPermissionApi = { authorize: jest.fn() }; diff --git a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.test.tsx b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.test.tsx index 2c0680b5f8..e125c96c98 100644 --- a/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatingExtensionsPage/TemplatingExtensionsPage.test.tsx @@ -40,6 +40,8 @@ const scaffolderApiMock: jest.Mocked = { listTemplatingExtensions, listTasks: jest.fn(), autocomplete: jest.fn(), + retry: jest.fn(), + dryRun: jest.fn(), }; const mockPermissionApi = { authorize: jest.fn() }; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 97c90b2aa5..4b272f1aa0 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -378,6 +378,100 @@ describe('', () => { }); }); + describe('ui:autoSelect behavior', () => { + beforeEach(() => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + }, + }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData, + } as unknown as FieldProps; + + catalogApi.getEntities.mockResolvedValue({ items: entities }); + }); + + it('default behavior', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type partial match and blur + fireEvent.change(input, { target: { value: 'team' } }); + fireEvent.blur(input); + + // Default behavior with freeSolo enabled processes the typed value + expect(onChange).toHaveBeenCalledWith('group:default/team'); + }); + + it('does not autoSelect value onBlur', async () => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + autoSelect: false, + }, + }; + props = { + ...props, + uiSchema, + } as unknown as FieldProps; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type and blur - with autoSelect=false, the autocomplete won't auto-select on blur + fireEvent.change(input, { target: { value: 'team' } }); + fireEvent.blur(input); + + // With autoSelect=false, onChange should not be called on blur + // This is the key difference - users must explicitly select an option + expect(onChange).not.toHaveBeenCalled(); + }); + + it('autoSelects entity onBlur', async () => { + uiSchema = { + 'ui:options': { + defaultKind: 'Group', + autoSelect: true, + }, + }; + props = { + ...props, + uiSchema, + } as unknown as FieldProps; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + // Type and blur + fireEvent.change(input, { target: { value: 'squad' } }); + fireEvent.blur(input); + + // With autoSelect=true and freeSolo, processes the typed value + expect(onChange).toHaveBeenCalledWith('group:default/squad'); + }); + }); + describe('uses full entity ref', () => { beforeEach(() => { uiSchema = { diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index d2197d4426..9a3dbd0009 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -74,6 +74,7 @@ export const EntityPicker = (props: EntityPickerProps) => { const defaultKind = uiSchema['ui:options']?.defaultKind; const defaultNamespace = uiSchema['ui:options']?.defaultNamespace || undefined; + const autoSelect = uiSchema?.['ui:options']?.autoSelect ?? true; const isDisabled = uiSchema?.['ui:disabled'] ?? false; const catalogApi = useApi(catalogApiRef); @@ -209,7 +210,7 @@ export const EntityPicker = (props: EntityPickerProps) => { : entities?.entityRefToPresentation.get(stringifyEntityRef(option)) ?.entityRef! } - autoSelect + autoSelect={autoSelect} freeSolo={allowArbitraryValues} renderInput={params => ( { const config = new ConfigReader({ integrations: { - bitbucket: [ + bitbucketCloud: [ { host: 'bitbucket.org', }, + ], + bitbucketServer: [ { host: 'server.bitbucket.com', }, diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 682fc6bb2a..2f3c5deeb9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -39,9 +39,16 @@ export const repoPickerValidation = ( 'Incomplete repository location provided, host not provided', ); } else { - if (integrationApi?.byHost(host)?.type === 'bitbucket') { + const integrationType = integrationApi?.byHost(host)?.type; + if ( + integrationType === 'bitbucketCloud' || + integrationType === 'bitbucketServer' + ) { // workspace is only applicable for bitbucket cloud - if (host === 'bitbucket.org' && !searchParams.get('workspace')) { + if ( + integrationType === 'bitbucketCloud' && + !searchParams.get('workspace') + ) { validation.addError( 'Incomplete repository location provided, workspace not provided', ); @@ -52,7 +59,7 @@ export const repoPickerValidation = ( 'Incomplete repository location provided, project not provided', ); } - } else if (integrationApi?.byHost(host)?.type === 'azure') { + } else if (integrationType === 'azure') { if (!searchParams.get('project')) { validation.addError( 'Incomplete repository location provided, project not provided', diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index 9efec70b20..7a367ad9ed 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-search-backend-module-catalog +## 0.3.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.3.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.3.12-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 038327fb77..5da79c0f94 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.3.12-next.1", + "version": "0.3.13-next.2", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 777405f8a0..d63ee5aea6 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 1.8.0 + +### Minor Changes + +- 583bd3a: Added `elasticsearchAuthExtensionPoint` to enable dynamic authentication mechanisms such as bearer tokens with automatic rotation. + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 7021165: Fixed bulk indexing to refresh only the target index instead of all indexes, improving performance in multi-index deployments. +- Updated dependencies + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + ## 1.8.0-next.1 ### Minor Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 14ca4d3298..0a4ea23ad3 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.8.0-next.1", + "version": "1.8.1-next.1", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index 616ce5db9d..6380150798 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-search-backend-module-explore +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.3.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- df27350: Updated dependency `@backstage-community/plugin-explore-common` to `^0.12.0`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + ## 0.3.11-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 607e562b25..575a6d17a6 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.3.11-next.1", + "version": "0.3.12-next.1", "description": "A module for the search backend that exports explore modules", "backstage": { "moved": "@backstage-community/plugin-search-backend-module-explore", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index fdb3e0068c..6efd6f0a67 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.53-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.5.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.5.52 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 2ee354a: Return `numberOfResults` count with search query responses +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + ## 0.5.52-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 3a018895ef..a751fa50f7 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.52-next.1", + "version": "0.5.53-next.1", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index 6c5618815f..751e890e99 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.3.17 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + ## 0.3.17-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index b469f7a215..f95e810740 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.3.17-next.0", + "version": "0.3.18-next.1", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 30c265720f..00671e4020 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.4.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-techdocs-node@1.14.4-next.2 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 0.4.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.4.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## 0.4.11 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/catalog-client@1.13.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-catalog-common@1.1.8 + ## 0.4.11-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 3088acd730..a412d44dfd 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.4.11-next.1", + "version": "0.4.12-next.2", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 1a2a84149a..ee42b8c809 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-search-backend-node +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + +## 1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-common@1.2.22 + +## 1.4.1 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + ## 1.4.1-next.0 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 9fb5687847..ad7f1f6948 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.4.1-next.0", + "version": "1.4.2-next.1", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index d2dfcf003f..ff9eda75fc 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-search-backend +## 2.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.1 + - @backstage/plugin-permission-node@0.10.11-next.1 + - @backstage/plugin-search-backend-node@1.4.2-next.1 + +## 2.1.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 2.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 2.0.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- 69d880e: Bump to latest zod to ensure it has the latest features +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.6 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-backend-node@1.4.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.10 + ## 2.0.12-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 143a26c4e9..c734c4e549 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "2.0.12-next.1", + "version": "2.1.0-next.2", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", diff --git a/plugins/search-backend/src/schema/openapi.yaml b/plugins/search-backend/src/schema/openapi.yaml index d0257a7ce0..7cb9da72c1 100644 --- a/plugins/search-backend/src/schema/openapi.yaml +++ b/plugins/search-backend/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: search version: '1' diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts index 26652de2cd..d3aa0447a8 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts index 8d81cbaf39..9a0ffed740 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/index.ts b/plugins/search-backend/src/schema/openapi/generated/index.ts index dec4b8804e..58fc52487a 100644 --- a/plugins/search-backend/src/schema/openapi/generated/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 809cd3fa80..d72822b815 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3914d73531..c4af31b48c 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts index 190ab8dc59..08abce9f46 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts index 7a1c9c155a..f7c9951f50 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts index 004ce76587..99f36fd9a4 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/index.ts b/plugins/search-backend/src/schema/openapi/generated/models/index.ts index fc9f1408f7..e8ada0c030 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-backend/src/schema/openapi/generated/router.ts b/plugins/search-backend/src/schema/openapi/generated/router.ts index a2510346ca..17f0f727e9 100644 --- a/plugins/search-backend/src/schema/openapi/generated/router.ts +++ b/plugins/search-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: 'search', version: '1', diff --git a/plugins/search-backend/src/schema/openapi/index.ts b/plugins/search-backend/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/plugins/search-backend/src/schema/openapi/index.ts +++ b/plugins/search-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index e64f4daae0..707180c009 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-common +## 1.2.22 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/plugin-permission-common@0.9.6 + ## 1.2.22-next.0 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index 69686d0d3c..39b7d63110 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-common", - "version": "1.2.22-next.0", + "version": "1.2.22", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "backstage": { "role": "common-library", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 989f02b988..cb5684d392 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-search-react +## 1.10.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 1.10.5-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## 1.10.3 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 1.10.3-next.2 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 521814edad..6d1ef34f4e 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.10.3-next.2", + "version": "1.10.5-next.1", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index dc129c6327..0d18b888e5 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -138,14 +138,13 @@ const useSearchContextValue = ( const result = useAsync(async (): Promise => { if (isFirstEmptyMount.current) { + isFirstEmptyMount.current = false; if (!term && !types.length && !Object.keys(filters).length) { return { results: [], numberOfResults: 0, }; } - - isFirstEmptyMount.current = false; } // Here we cancel the previous request before making a new one diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index e53166a468..b2cf761251 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-search +## 1.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-search-react@1.10.5-next.1 + +## 1.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + +## 1.6.2-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## 1.6.0 + +### Minor Changes + +- feef8d9: Added support for configuring the default search type in the search page via the `search.defaultType` option in `app-config.yaml`. This applies to both the legacy and new frontend systems. If not set, the default is empty, which means searching for "all" types. + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-react@1.10.3 + ## 1.6.0-next.2 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 6e117e4898..4e731a8769 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.6.0-next.2", + "version": "1.6.2-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index 2e82c77416..e3f2d6108a 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -11,6 +11,7 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -68,22 +69,68 @@ const _default: OverridableFrontendPlugin< config: { noTrack: boolean; path: string | undefined; + title: string | undefined; }; configInput: { noTrack?: boolean | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; items: ExtensionInput< ConfigurableExtensionDataRef< { @@ -134,10 +181,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; } @@ -189,22 +238,68 @@ export const searchPage: OverridableExtensionDefinition<{ config: { noTrack: boolean; path: string | undefined; + title: string | undefined; }; configInput: { noTrack?: boolean | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; items: ExtensionInput< ConfigurableExtensionDataRef< { @@ -255,10 +350,12 @@ export const searchPage: OverridableExtensionDefinition<{ kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index c36ae54d33..9625f5e946 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -276,6 +276,8 @@ export const searchNavItem = NavItemBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'search', + title: 'Search', + icon: , info: { packageJson: () => import('../package.json') }, extensions: [searchApi, searchPage, searchNavItem], routes: { diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index 8428fb6bb5..92b327207f 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -59,9 +59,7 @@ export const UrlUpdater = () => { setPageCursor(query.pageCursor as string); } - if (query.types) { - setTypes(query.types as string[]); - } + setTypes(query.types ? (query.types as string[]) : []); }, [prevQueryParams, location, setTerm, setTypes, setPageCursor, setFilters]); useEffect(() => { diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index 6e98a06972..c8355f80a0 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-signals-backend +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-events-node@0.4.20-next.1 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## 0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## 0.3.12 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-events-node@0.4.19 + - @backstage/plugin-signals-node@0.1.28 + ## 0.3.12-next.0 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 9434b07327..6572aa79da 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.3.12-next.0", + "version": "0.3.13-next.1", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index 257a3e8125..3c503bf17e 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-signals-node +## 0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-events-node@0.4.20-next.1 + +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-events-node@0.4.19 + ## 0.1.28-next.0 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index c9d136dc5b..3620850c98 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.28-next.0", + "version": "0.1.29-next.1", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/signals-react/CHANGELOG.md b/plugins/signals-react/CHANGELOG.md index 690a33db5b..c21647cc29 100644 --- a/plugins/signals-react/CHANGELOG.md +++ b/plugins/signals-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-signals-react +## 0.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## 0.0.19 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-plugin-api@1.12.3 + ## 0.0.19-next.1 ### Patch Changes diff --git a/plugins/signals-react/package.json b/plugins/signals-react/package.json index 6b5408bb15..12723e0bf3 100644 --- a/plugins/signals-react/package.json +++ b/plugins/signals-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-react", - "version": "0.0.19-next.1", + "version": "0.0.20-next.0", "description": "Web library for the signals plugin", "backstage": { "role": "web-library", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 5fb4760909..c4bdff1556 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-signals +## 0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 0.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## 0.0.28 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-signals-react@0.0.19 + ## 0.0.28-next.2 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index adeda5d096..c29a2fd4ac 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.28-next.2", + "version": "0.0.29-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 8d47f5ab5a..910bd87be8 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-techdocs-addons-test-utils +## 2.0.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/plugin-techdocs@1.17.1-next.2 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/plugin-catalog@2.0.0-next.2 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 2.0.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 2.0.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 2.0.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/plugin-catalog@1.33.0 + - @backstage/core-app-api@1.19.5 + - @backstage/plugin-techdocs@1.17.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-search-react@1.10.3 + - @backstage/test-utils@1.7.15 + ## 2.0.2-next.2 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 904a85f58b..c9a4e1821c 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "2.0.2-next.2", + "version": "2.0.3-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index ca5c2106ab..336904ffaf 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-techdocs-backend +## 2.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-techdocs-node@1.14.4-next.2 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/plugin-catalog-node@2.1.0-next.2 + +## 2.1.6-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 2.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## 2.1.5 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations. +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/plugin-catalog-node@2.0.0 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-techdocs-node@1.14.2 + - @backstage/catalog-client@1.13.0 + ## 2.1.5-next.2 ### Patch Changes diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 5d3baa940a..87369f2ccd 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -64,6 +64,18 @@ export interface Config { * List of mkdocs plugins which should be added as default to all mkdocs.yml files. */ defaultPlugins?: string[]; + + /** + * List of additional MkDocs configuration keys to allow beyond + * the default safe allowlist. This can introduce security vulnerabilities. + * + * WARNING: Some MkDocs configuration keys can execute arbitrary code. For example, the + * 'hooks' key allows running arbitrary Python code during documentation generation. + * Only use this in trusted environments where all mkdocs.yml files are audited. + * + * @see https://www.mkdocs.org/user-guide/configuration/#hooks + */ + dangerouslyAllowAdditionalKeys?: string[]; }; }; diff --git a/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md index f41ab1d506..d1172aaceb 100644 --- a/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md +++ b/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md @@ -10,7 +10,7 @@ const serviceEntityPage = ( - + diff --git a/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md index f41ab1d506..d1172aaceb 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md @@ -10,7 +10,7 @@ const serviceEntityPage = ( - + diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 72e57e0eef..6615e0831f 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "2.1.5-next.2", + "version": "2.1.6-next.2", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 48ef41fb4e..9a03452bdc 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.34-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 1.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.1.33 + +### Patch Changes + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + ## 1.1.33-next.2 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 8d79dfc75b..861344567a 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.33-next.2", + "version": "1.1.34-next.2", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx index 0f618013f1..b4084c6411 100644 --- a/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx @@ -56,4 +56,43 @@ describe('LightBox', () => { expect.any(Function), ); }); + + it('does not add onclick event to linked images', async () => { + await TechDocsAddonTester.buildAddonsInTechDocs([]) + .withDom( + , + ) + .withApis([[entityPresentationApiRef, entityPresentationApiMock]]) + .renderWithEffects(); + + expect(screen.getByShadowTestId('linked-fixture').onclick).toBeNull(); + expect( + screen.getByShadowTestId('linked-indirect-fixture').onclick, + ).toBeNull(); + expect(screen.getByShadowTestId('plain-fixture').onclick).toEqual( + expect.any(Function), + ); + }); }); diff --git a/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx index 47409f47c7..89fe842a15 100644 --- a/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx +++ b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx @@ -27,6 +27,7 @@ export const LightBoxAddon = () => { useEffect(() => { let dataSourceImages: DataSource | null = null; + const lightboxImages = images.filter(image => !image.closest('a')); let lightbox: PhotoSwipeLightbox | null = new PhotoSwipeLightbox({ pswpModule: PhotoSwipe, @@ -57,10 +58,10 @@ export const LightBoxAddon = () => { `, }); - images.forEach((image, index) => { + lightboxImages.forEach((image, index) => { image.onclick = () => { if (dataSourceImages === null) { - dataSourceImages = images.map(dataSourceImage => { + dataSourceImages = lightboxImages.map(dataSourceImage => { return { element: dataSourceImage, src: dataSourceImage.src, diff --git a/plugins/techdocs-module-addons-contrib/src/plugin.ts b/plugins/techdocs-module-addons-contrib/src/plugin.ts index ac19d682de..6ed09a7db9 100644 --- a/plugins/techdocs-module-addons-contrib/src/plugin.ts +++ b/plugins/techdocs-module-addons-contrib/src/plugin.ts @@ -211,6 +211,8 @@ export const TextSize = techdocsModuleAddonsContribPlugin.provide( * @remarks * The image size of the lightbox image is the same as the image size on the document page. * + * Images that are wrapped in links are ignored to avoid blocking navigation. + * * @example * Here's a simple example: * ``` diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 3514db3e48..360a1612c7 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-techdocs-node +## 1.14.4-next.2 + +### Patch Changes + +- e96f6d9: Removed `INHERIT` from the `ALLOWED_MKDOCS_KEYS` set to address a security concern with MkDocs configuration inheritance. +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/integration@2.0.0-next.2 + +## 1.14.3-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.14.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.14.2 + +### Patch Changes + +- 7455dae: Use node prefix on native imports +- 3c455d4: Some security fixes +- Updated dependencies + - @backstage/integration@1.20.0 + - @backstage/integration-aws-node@0.1.20 + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-search-common@1.2.22 + ## 1.14.2-next.1 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index d359d7b5d4..183cab17b5 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.14.2-next.1", + "version": "1.14.4-next.2", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index fd428fd23a..1f198b43e9 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -885,6 +885,32 @@ another_unknown: true ); }); + it('should remove the INHERIT key to prevent loading unsanitized parent configs', async () => { + const mkdocsWithInherit = `INHERIT: ../parent.yml +site_name: Test +`; + mockDir.setContent({ + 'mkdocs_inherit.yml': mkdocsWithInherit, + }); + + await sanitizeMkdocsYml( + mockDir.resolve('mkdocs_inherit.yml'), + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_inherit.yml'), + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as Record< + string, + unknown + >; + + expect(parsedYml.INHERIT).toBeUndefined(); + expect(parsedYml.site_name).toBe('Test'); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('INHERIT')); + }); + it('should remove hooks with duplicate merge keys and top-level anchors', async () => { mockDir.setContent({ 'mkdocs_duplicate_merge.yml': mkdocsYmlWithDuplicateMergeHooks, @@ -910,6 +936,83 @@ another_unknown: true expect(warn).toHaveBeenCalledWith(expect.stringContaining('hooks')); }); + + it('should allow additional keys when configured via dangerouslyAllowAdditionalKeys', async () => { + const mkdocsWithHooksAndCustom = `site_name: Test +hooks: + - hook.py +custom_dir: custom +some_unknown_key: value +`; + mockDir.setContent({ + 'mkdocs_allowed.yml': mkdocsWithHooksAndCustom, + }); + + // Allow 'hooks' and 'custom_dir' but not 'some_unknown_key' + await sanitizeMkdocsYml( + mockDir.resolve('mkdocs_allowed.yml'), + mockLogger, + ['hooks', 'custom_dir'], + ); + + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_allowed.yml'), + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as Record< + string, + unknown + >; + + // 'hooks' and 'custom_dir' should be preserved + expect(parsedYml.hooks).toEqual(['hook.py']); + expect(parsedYml.custom_dir).toBe('custom'); + // 'some_unknown_key' should still be removed + expect(parsedYml.some_unknown_key).toBeUndefined(); + expect(parsedYml.site_name).toBe('Test'); + + // Should warn about the dangerous configuration AND the removed key + expect(warn).toHaveBeenCalledWith( + expect.stringContaining( + 'DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: hooks, custom_dir', + ), + ); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining( + 'Removed the following unsupported configuration keys from mkdocs.yml: some_unknown_key', + ), + ); + }); + + it('should warn about dangerous keys even when no keys are removed', async () => { + mockDir.setContent({ + 'mkdocs_with_hooks.yml': mkdocsYmlWithHooks, + }); + + await sanitizeMkdocsYml( + mockDir.resolve('mkdocs_with_hooks.yml'), + mockLogger, + ['hooks'], + ); + + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_hooks.yml'), + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + hooks?: string[]; + site_name: string; + }; + + // Hooks should be preserved + expect(parsedYml.hooks).toBeDefined(); + expect(parsedYml.site_name).toBe('Test site name'); + + // Should warn about dangerous configuration + expect(warn).toHaveBeenCalledWith( + expect.stringContaining( + 'DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: hooks', + ), + ); + }); }); describe('validateDocsDirectory', () => { diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index 1b0813f18d..caecb47524 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -309,7 +309,6 @@ export const ALLOWED_MKDOCS_KEYS = new Set([ 'validation', // Deprecated 'google_analytics', - 'INHERIT', ]); /** diff --git a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts index cd506d3b28..d676237b01 100644 --- a/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts +++ b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts @@ -198,15 +198,28 @@ export const patchMkdocsYmlWithPlugins = async ( * * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site * @param logger - A logger instance + * @param additionalAllowedKeys - Optional array of additional keys to allow beyond the default allowlist */ export const sanitizeMkdocsYml = async ( mkdocsYmlPath: string, logger: LoggerService, + additionalAllowedKeys?: string[], ) => { await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + // Combine default allowed keys with additional keys + const allowedKeys = new Set(ALLOWED_MKDOCS_KEYS); + if (additionalAllowedKeys && additionalAllowedKeys.length > 0) { + logger.warn( + `DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: ${additionalAllowedKeys.join( + ', ', + )}. This may introduce security vulnerabilities. Only use in trusted environments.`, + ); + additionalAllowedKeys.forEach(key => allowedKeys.add(key)); + } + // Identify keys that will be removed for logging const removedKeys = Object.keys(mkdocsYml).filter( - key => !ALLOWED_MKDOCS_KEYS.has(key), + key => !allowedKeys.has(key), ); if (removedKeys.length > 0) { @@ -220,7 +233,7 @@ export const sanitizeMkdocsYml = async ( // Build a new object with only allowed keys const sanitized: Record = {}; - for (const key of ALLOWED_MKDOCS_KEYS) { + for (const key of allowedKeys) { if (key in mkdocsYml) { sanitized[key] = (mkdocsYml as Record)[key]; } diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index ed357c5c81..8a764eb391 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -115,7 +115,11 @@ export class TechdocsGenerator implements GeneratorBase { const docsDir = await validateMkdocsYaml(inputDir, content); // Remove unsupported configuration keys - await sanitizeMkdocsYml(mkdocsYmlPath, childLogger); + await sanitizeMkdocsYml( + mkdocsYmlPath, + childLogger, + this.options.dangerouslyAllowAdditionalKeys, + ); // Validate that no symlinks in the docs directory point outside the input directory // This prevents path traversal attacks where malicious symlinks could leak host files @@ -257,5 +261,8 @@ export function readGeneratorConfig( defaultPlugins: config.getOptionalStringArray( 'techdocs.generator.mkdocs.defaultPlugins', ), + dangerouslyAllowAdditionalKeys: config.getOptionalStringArray( + 'techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys', + ), }; } diff --git a/plugins/techdocs-node/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts index 418260b819..b716e9510b 100644 --- a/plugins/techdocs-node/src/stages/generate/types.ts +++ b/plugins/techdocs-node/src/stages/generate/types.ts @@ -45,6 +45,7 @@ export type GeneratorConfig = { omitTechdocsCoreMkdocsPlugin?: boolean; legacyCopyReadmeMdToIndexMd?: boolean; defaultPlugins?: string[]; + dangerouslyAllowAdditionalKeys?: string[]; }; /** diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 7dcc249e8a..6f21db2b77 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-techdocs-react +## 1.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/core-components@0.18.8-next.1 + +## 1.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-techdocs-common@0.1.1 + +## 1.3.8 + +### Patch Changes + +- 22dce2b: TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + + As part of this change, the `techDocsAddonDataRef` alpha export was removed. + +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/core-components@0.18.7 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/version-bridge@1.0.12 + ## 1.3.8-next.1 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 6c4f934d77..4e6b168ea9 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.3.8-next.1", + "version": "1.3.9-next.1", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index a320099d0d..a5971aa8ba 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,105 @@ # @backstage/plugin-techdocs +## 1.17.1-next.2 + +### Patch Changes + +- 9795d30: chore(deps): bump `dompurify` from 3.3.1 to 3.3.2 +- Updated dependencies + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/catalog-client@1.14.0-next.2 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/integration@2.0.0-next.2 + - @backstage/core-components@0.18.8-next.1 + - @backstage/plugin-search-react@1.10.5-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.1 + +## 1.17.1-next.1 + +### Patch Changes + +- 30e08df: Added `documentation` as the default entity content group for the TechDocs entity content tab. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.17.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 1.17.0 + +### Minor Changes + +- 27798df: Add two config values to the `page:techdocs/reader` extension that configure default layout, `withoutSearch` and `withoutHeader`. Default are unchanged to `false`. + + E.g. to disable the search and header on the Techdocs Reader Page: + + ```yaml + app: + extensions: + - page:techdocs/reader: + config: + withoutSearch: true + withoutHeader: true + ``` + +### Patch Changes + +- 7feb83b: Adjusted to use the new `@backstage/filter-predicates` types for predicate expressions. +- 491a06c: Add the ability to show icons for the tabs on the entity page (new frontend) +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- 9e29545: Improve sidebars (nav/TOC) layout and scrolling +- 22dce2b: TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- 0a88779: Added title prop to OffsetPaginatedDocsTable for proper display +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/integration@1.20.0 + - @backstage/core-components@0.18.7 + - @backstage/plugin-search-common@1.2.22 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/catalog-client@1.13.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-techdocs-react@1.3.8 + - @backstage/integration-react@1.2.15 + - @backstage/plugin-search-react@1.10.3 + - @backstage/plugin-auth-react@0.1.24 + ## 1.16.3-next.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index e31898aaea..5ec3c79f7d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.16.3-next.2", + "version": "1.17.1-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", @@ -80,7 +80,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", "@microsoft/fetch-event-source": "^2.0.1", - "dompurify": "^3.2.4", + "dompurify": "^3.3.2", "git-url-parse": "^15.0.0", "lodash": "^4.17.21", "react-helmet": "6.1.0", diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index 3092a010d4..aa14098288 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -14,6 +14,7 @@ import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { FilterPredicate } from '@backstage/filter-predicates'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { IconLinkVerticalProps } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { JSXElementConstructor } from 'react'; @@ -139,7 +140,6 @@ const _default: OverridableFrontendPlugin< }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', @@ -147,6 +147,7 @@ const _default: OverridableFrontendPlugin< optional: true; } > + | ExtensionDataRef | ExtensionDataRef< (entity: Entity) => boolean, 'catalog.entity-filter-function', @@ -284,26 +285,75 @@ const _default: OverridableFrontendPlugin< name: undefined; config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; - inputs: {}; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; 'page:techdocs/reader': OverridableExtensionDefinition<{ @@ -311,23 +361,69 @@ const _default: OverridableFrontendPlugin< withoutSearch: boolean; withoutHeader: boolean; path: string | undefined; + title: string | undefined; }; configInput: { withoutSearch?: boolean | undefined; withoutHeader?: boolean | undefined; + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef_2, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; addons: ExtensionInput< ConfigurableExtensionDataRef< TechDocsAddonOptions, @@ -344,10 +440,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: 'reader'; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef_2; + noHeader?: boolean; }; }>; 'search-result-list-item:techdocs': OverridableExtensionDefinition<{ diff --git a/plugins/techdocs/report.api.md b/plugins/techdocs/report.api.md index e7e9e4b916..9e4ed4ce45 100644 --- a/plugins/techdocs/report.api.md +++ b/plugins/techdocs/report.api.md @@ -66,11 +66,7 @@ export type ContentStateTypes = | 'CONTENT_FRESH'; // @public -export const CustomDocsPanel: ({ - config, - entities, - index, -}: { +export const CustomDocsPanel: (input: { config: PanelConfig; entities: Entity[]; index: number; @@ -151,12 +147,11 @@ export type DocsTableRow = { }; // @public -export const EmbeddedDocsRouter: ({ - children, - withSearch, -}: PropsWithChildren<{ - withSearch?: boolean; -}>) => JSX_2.Element; +export const EmbeddedDocsRouter: ( + input: PropsWithChildren<{ + withSearch?: boolean; + }>, +) => JSX_2.Element; // @public export const EntityListDocsGrid: ( @@ -208,12 +203,11 @@ export type EntityListDocsTableProps = { }; // @public -export const EntityTechdocsContent: ({ - children, - withSearch, -}: PropsWithChildren<{ - withSearch?: boolean; -}>) => JSX_2.Element; +export const EntityTechdocsContent: ( + input: PropsWithChildren<{ + withSearch?: boolean; + }>, +) => JSX_2.Element; // @public export const InfoCardGrid: (props: InfoCardGridProps) => JSX_2.Element | null; diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index 9e63004046..1ec92edfd3 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -223,6 +223,7 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ { path: 'docs', title: 'TechDocs', + group: 'documentation', routeRef: rootCatalogDocsRouteRef, loader: () => { // Merge addons from the API with old-style direct attachments @@ -278,6 +279,8 @@ const techDocsNavItem = NavItemBlueprint.make({ /** @alpha */ export default createFrontendPlugin({ pluginId: 'techdocs', + title: 'Docs', + icon: , info: { packageJson: () => import('../../package.json') }, extensions: [ techDocsClientApi, diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index 8922a1855f..14946104c6 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -415,39 +415,44 @@ describe('useReaderState', () => { }); it('should handle stale content', async () => { - techdocsStorageApi.getEntityDocs - .mockResolvedValueOnce('my content') - .mockImplementationOnce(async () => { - await new Promise(resolve => setTimeout(resolve, 1100)); - return 'my new content'; + jest.useFakeTimers(); + + try { + techdocsStorageApi.getEntityDocs + .mockResolvedValueOnce('my content') + .mockImplementationOnce(async () => { + await new Promise(resolve => setTimeout(resolve, 1100)); + return 'my new content'; + }); + techdocsStorageApi.syncEntityDocs.mockImplementation( + async (_, logHandler) => { + await 'a tick'; + logHandler?.call(this, 'Line 1'); + logHandler?.call(this, 'Line 2'); + await new Promise(resolve => setTimeout(resolve, 1100)); + return 'updated'; + }, + ); + + const { result } = renderHook( + () => useReaderState('Component', 'default', 'backstage', '/example'), + { wrapper: Wrapper }, + ); + + expect(result.current).toEqual({ + state: 'CHECKING', + path: '/example', + content: undefined, + contentErrorMessage: undefined, + syncErrorMessage: undefined, + buildLog: [], + contentReload: expect.any(Function), }); - techdocsStorageApi.syncEntityDocs.mockImplementation( - async (_, logHandler) => { - await 'a tick'; - logHandler?.call(this, 'Line 1'); - logHandler?.call(this, 'Line 2'); - await new Promise(resolve => setTimeout(resolve, 1100)); - return 'updated'; - }, - ); - const { result } = renderHook( - () => useReaderState('Component', 'default', 'backstage', '/example'), - { wrapper: Wrapper }, - ); + // flush microtasks: content loads (resolved promise) and sync progresses past 'a tick' + await act(async () => {}); - expect(result.current).toEqual({ - state: 'CHECKING', - path: '/example', - content: undefined, - contentErrorMessage: undefined, - syncErrorMessage: undefined, - buildLog: [], - contentReload: expect.any(Function), - }); - - // the content is returned but the sync is in progress - await waitFor(() => { + // the content is returned but the sync is in progress expect(result.current).toEqual({ state: 'CONTENT_FRESH', path: '/example', @@ -457,10 +462,12 @@ describe('useReaderState', () => { buildLog: ['Line 1', 'Line 2'], contentReload: expect.any(Function), }); - }); - // the sync takes longer than 1 seconds so the refreshing state starts - await waitFor(() => { + // the sync takes longer than 1 second so the refreshing state starts + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current).toEqual({ state: 'CONTENT_STALE_REFRESHING', path: '/example', @@ -470,10 +477,12 @@ describe('useReaderState', () => { buildLog: ['Line 1', 'Line 2'], contentReload: expect.any(Function), }); - }); - // the content is updated but not yet displayed - await waitFor(() => { + // the sync completes — content is updated but not yet displayed + await act(async () => { + jest.advanceTimersByTime(100); + }); + expect(result.current).toEqual({ state: 'CONTENT_STALE_READY', path: '/example', @@ -483,15 +492,13 @@ describe('useReaderState', () => { buildLog: ['Line 1', 'Line 2'], contentReload: expect.any(Function), }); - }); - // reload the content - await act(async () => { - result.current.contentReload(); - }); + // reload the content + await act(async () => { + result.current.contentReload(); + }); - // the new content refresh is triggered - await waitFor(() => { + // the new content refresh is triggered expect(result.current).toEqual({ state: 'CHECKING', path: '/example', @@ -501,39 +508,38 @@ describe('useReaderState', () => { buildLog: [], contentReload: expect.any(Function), }); - }); - // the new content is loaded - await waitFor( - () => { - expect(result.current).toEqual({ - state: 'CONTENT_FRESH', - path: '/example', - content: 'my new content', - contentErrorMessage: undefined, - syncErrorMessage: undefined, - buildLog: [], - contentReload: expect.any(Function), - }); - }, - { - timeout: 2000, - }, - ); + // the new content is loaded + await act(async () => { + jest.advanceTimersByTime(1100); + }); - expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2); - expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith( - { kind: 'Component', namespace: 'default', name: 'backstage' }, - '/example', - ); - expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith( - { - kind: 'Component', - namespace: 'default', - name: 'backstage', - }, - expect.any(Function), - ); + expect(result.current).toEqual({ + state: 'CONTENT_FRESH', + path: '/example', + content: 'my new content', + contentErrorMessage: undefined, + syncErrorMessage: undefined, + buildLog: [], + contentReload: expect.any(Function), + }); + + expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2); + expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith( + { kind: 'Component', namespace: 'default', name: 'backstage' }, + '/example', + ); + expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith( + { + kind: 'Component', + namespace: 'default', + name: 'backstage', + }, + expect.any(Function), + ); + } finally { + jest.useRealTimers(); + } }); it('should handle navigation', async () => { diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 8e7bf3158a..8c7deee0a6 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-user-settings-backend +## 0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.8.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.2 + - @backstage/plugin-signals-node@0.1.29-next.1 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## 0.4.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + +### Patch Changes + +- 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`. +- Updated dependencies + - @backstage/backend-plugin-api@1.7.0 + - @backstage/plugin-auth-node@0.6.13 + - @backstage/plugin-user-settings-common@0.1.0 + - @backstage/plugin-signals-node@0.1.28 + ## 0.4.0-next.1 ### Minor Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 732922c501..e512de98c4 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.4.0-next.1", + "version": "0.4.1-next.1", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings-common/CHANGELOG.md b/plugins/user-settings-common/CHANGELOG.md index 658ad6e85a..55755c386b 100644 --- a/plugins/user-settings-common/CHANGELOG.md +++ b/plugins/user-settings-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-user-settings-common +## 0.1.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/user-settings-common/package.json b/plugins/user-settings-common/package.json index d190794c81..161c370c70 100644 --- a/plugins/user-settings-common/package.json +++ b/plugins/user-settings-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-common", - "version": "0.1.0-next.0", + "version": "0.1.0", "description": "Common functionalities for the user-settings plugin", "backstage": { "role": "common-library", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 7ef098b36a..c45eee08d5 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-user-settings +## 0.9.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.1 + - @backstage/frontend-plugin-api@0.15.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.2 + - @backstage/core-components@0.18.8-next.1 + +## 0.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## 0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## 0.9.0 + +### Minor Changes + +- 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance + +### Patch Changes + +- 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system. +- a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings. +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.0 + - @backstage/core-components@0.18.7 + - @backstage/core-app-api@1.19.5 + - @backstage/theme@0.7.2 + - @backstage/frontend-plugin-api@0.14.0 + - @backstage/core-plugin-api@1.12.3 + - @backstage/plugin-user-settings-common@0.1.0 + - @backstage/plugin-signals-react@0.0.19 + ## 0.9.0-next.2 ### Minor Changes diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index a27e5ca524..8644001fe1 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -11,7 +11,20 @@ be used in the frontend as a persistent alternative to the builtin `WebStorage`. Please see [the backend README](https://github.com/backstage/backstage/tree/master/plugins/user-settings-backend) for installation instructions. -## Components Usage +## Installation + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-user-settings +``` + +Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](https://backstage.io/docs/frontend-system/building-apps/installing-plugins). + +## Old Frontend System + +If your Backstage app uses the old frontend system, you need to manually wire the plugin into your app as outlined in this section. If you are on the new frontend system, you can skip this. + +### Components Usage Add the item to the Sidebar: diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 20208bcbd0..a4d5d505b7 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.9.0-next.2", + "version": "0.9.1-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 056b5ee557..c99709d777 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -8,6 +8,7 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -46,21 +47,67 @@ const _default: OverridableFrontendPlugin< 'page:user-settings': OverridableExtensionDefinition<{ config: { path: string | undefined; + title: string | undefined; }; configInput: { + title?: string | undefined; path?: string | undefined; }; output: | ExtensionDataRef - | ExtensionDataRef | ExtensionDataRef< RouteRef, 'core.routing.ref', { optional: true; } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } >; inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; providerSettings: ExtensionInput< ConfigurableExtensionDataRef, { @@ -73,10 +120,12 @@ const _default: OverridableFrontendPlugin< kind: 'page'; name: undefined; params: { - defaultPath?: [Error: `Use the 'path' param instead`]; path: string; - loader: () => Promise; + title?: string; + icon?: IconElement; + loader?: () => Promise; routeRef?: RouteRef; + noHeader?: boolean; }; }>; } diff --git a/plugins/user-settings/report.api.md b/plugins/user-settings/report.api.md index 77670bb357..2994f2c13c 100644 --- a/plugins/user-settings/report.api.md +++ b/plugins/user-settings/report.api.md @@ -11,6 +11,7 @@ import { ElementType } from 'react'; import { ErrorApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react/jsx-runtime'; @@ -35,7 +36,7 @@ export const DefaultProviderSettings: (props: { export const ProviderSettingsItem: (props: { title: string; description: string; - icon: IconComponent; + icon: IconComponent | IconElement; apiRef: ApiRef; }) => JSX_2.Element; diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index bea75bbdd8..87b858fcec 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -62,6 +62,8 @@ export const settingsNavItem = NavItemBlueprint.make({ */ export default createFrontendPlugin({ pluginId: 'user-settings', + title: 'Settings', + icon: , info: { packageJson: () => import('../package.json') }, extensions: [userSettingsPage, settingsNavItem], routes: { diff --git a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx index 32eaeb0cb9..534e1dc5ad 100644 --- a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx +++ b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEffect, useState } from 'react'; +import { createElement, isValidElement, useEffect, useState } from 'react'; import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import ListItem from '@material-ui/core/ListItem'; @@ -33,6 +33,7 @@ import { errorApiRef, IconComponent, } from '@backstage/core-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; import { ProviderSettingsAvatar } from './ProviderSettingsAvatar'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { userSettingsTranslationRef } from '../../translation'; @@ -43,10 +44,10 @@ const emptyProfile: ProfileInfo = {}; export const ProviderSettingsItem = (props: { title: string; description: string; - icon: IconComponent; + icon: IconComponent | IconElement; apiRef: ApiRef; }) => { - const { title, description, icon: Icon, apiRef } = props; + const { title, description, icon, apiRef } = props; const api = useApi(apiRef); const errorApi = useApi(errorApiRef); @@ -86,11 +87,14 @@ export const ProviderSettingsItem = (props: { }; }, [api]); + const iconElement = + icon === null || isValidElement(icon) + ? icon + : createElement(icon as IconComponent); + return ( - - - + {iconElement} f.trim()); + + const mdFiles = prFiles + .filter(f => f.endsWith('.md')) + .filter(f => !IGNORED_WHEN_LISTING.some(p => p.test(f))); + + if (mdFiles.length === 0) { + console.log('No documentation files to check.'); + return; + } + + console.log(`Checking ${mdFiles.length} changed documentation file(s)...`); + + const result = spawnSync( + 'vale', + [ + '--config', + resolvePath(rootDir, '.vale.ini'), + '--output=JSON', + ...mdFiles, + ], + { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }, + ); + + if (result.error) { + console.error('Failed to run vale:', result.error.message); + process.exit(1); + } + + let issueCount = 0; + + if (result.stdout && result.stdout.trim()) { + try { + const data = JSON.parse(result.stdout); + for (const [file, alerts] of Object.entries(data)) { + for (const alert of alerts) { + const severityLevels = { + error: 'error', + warning: 'warning', + suggestion: 'notice', + }; + const level = severityLevels[alert.Severity] ?? 'notice'; + const col = alert.Span ? alert.Span[0] : 1; + const endCol = alert.Span ? `,endColumn=${alert.Span[1] + 1}` : ''; + console.log( + `::${level} file=${file},line=${alert.Line},col=${col}${endCol},title=${alert.Check}::${alert.Message}`, + ); + issueCount++; + } + } + } catch { + console.error('Failed to parse vale output:'); + console.error(result.stdout); + process.exit(1); + } + } + + if (result.stderr && result.stderr.trim()) { + console.error(result.stderr); + } + + if (issueCount > 0) { + console.log( + `\nFound ${issueCount} documentation quality issue(s). Please review the annotations above.`, + ); + } + + if (result.status !== 0) { + process.exit(1); + } +} + async function main() { + if (process.argv.includes('--ci')) { + const idx = process.argv.indexOf('--ci'); + const prFilesPath = process.argv[idx + 1]; + if (!prFilesPath) { + console.error('Usage: check-docs-quality.js --ci '); + process.exit(1); + } + await ciCheck(prFilesPath); + return; + } + if (process.argv.includes('--ci-args')) { const files = await listFiles(); diff --git a/scripts/plugin-directory-audit.js b/scripts/plugin-directory-audit.js index dcdf295d92..c96b82354c 100644 --- a/scripts/plugin-directory-audit.js +++ b/scripts/plugin-directory-audit.js @@ -34,15 +34,17 @@ function getAge(npmModified) { return Math.round(ageDif / (1000 * 60 * 60 * 24)); } -async function main() { +async function main(args) { const rootPath = resolve(__dirname, '..'); const pluginDataPath = resolve(rootPath, 'microsite/data/plugins'); - console.log(__dirname, rootPath, pluginDataPath); + const auditMode = args.includes('--audit'); const pluginDataFiles = fs.readdirSync(pluginDataPath); const pluginsData = []; + const updatedPlugins = []; + for (const pluginDataFile of pluginDataFiles) { const pluginDataFilePath = resolve(pluginDataPath, pluginDataFile); const pluginDataYaml = yaml.load( @@ -54,18 +56,89 @@ async function main() { ); const npmPackage = await getNpmPackage(pluginDataYaml.npmPackageName); + const modifiedTime = npmPackage.time?.modified; + const age = getAge(npmPackage.time?.modified); + + if (!modifiedTime || isNaN(age)) { + console.warn( + `Skipping ${pluginDataYaml.title}: Could not calculate age (Data: ${modifiedTime})`, + ); + continue; // Skip to the next plugin in the loop + } const pluginData = { npmPackageName: pluginDataYaml.npmPackageName, npmCreated: npmPackage.time?.created, npmModified: npmPackage.time?.modified, - age: getAge(npmPackage.time?.modified), + age: age, + currentStatus: pluginDataYaml.status, }; + // Update plugin YAML if in audit mode + if (auditMode) { + let newStatus = pluginDataYaml.status; + let statusChanged = false; + + // If age < 365 and status is inactive or archived, change to active + if ( + age < 365 && + (pluginDataYaml.status === 'inactive' || + pluginDataYaml.status === 'archived') + ) { + newStatus = 'active'; + statusChanged = true; + } + // If status is inactive, change to archived + else if (age > 365 && pluginDataYaml.status === 'inactive') { + newStatus = 'archived'; + statusChanged = true; + } + // If age > 365 and status is active, change to inactive + else if (age > 365 && pluginDataYaml.status === 'active') { + newStatus = 'inactive'; + statusChanged = true; + } + + if (statusChanged) { + pluginDataYaml.status = newStatus; + pluginDataYaml.staleSince = new Date().toISOString().split('T')[0]; + + // Write updated YAML back to file + const yamlContent = yaml.dump(pluginDataYaml, { + lineWidth: -1, + quotingType: "'", + forceQuotes: false, + }); + fs.writeFileSync(pluginDataFilePath, `---\n${yamlContent}`); + + updatedPlugins.push({ + file: pluginDataFile, + plugin: pluginDataYaml.title, + oldStatus: pluginData.currentStatus, + newStatus: newStatus, + age: age, + }); + + console.log( + ` ✓ Updated ${pluginDataFile}: ${pluginData.currentStatus} → ${newStatus} (age: ${age} days)`, + ); + } + + pluginData.newStatus = newStatus; + } + pluginsData.push(pluginData); } console.table(pluginsData); + + if (auditMode && updatedPlugins.length > 0) { + console.log('\n=== Summary of Updates ==='); + console.table(updatedPlugins); + console.log(`\nTotal plugins updated: ${updatedPlugins.length}`); + } else if (auditMode) { + console.log('\nNo plugins required updates.'); + } } main(process.argv.slice(2)).catch(error => { diff --git a/scripts/verify-local-dependencies.js b/scripts/verify-local-dependencies.js index de7e2908f6..baf9564faf 100755 --- a/scripts/verify-local-dependencies.js +++ b/scripts/verify-local-dependencies.js @@ -62,6 +62,7 @@ const roleRules = [ // TODO(freben): Address these '@backstage/frontend-defaults', '@backstage/frontend-app-api', + '@backstage/frontend-dev-utils', '@backstage/frontend-test-utils', '@backstage/plugin-api-docs', '@backstage/plugin-techdocs-addons-test-utils', diff --git a/scripts/verify-plugin-directory.js b/scripts/verify-plugin-directory.js index b50c87ff42..24c068b96e 100644 --- a/scripts/verify-plugin-directory.js +++ b/scripts/verify-plugin-directory.js @@ -31,6 +31,8 @@ const configSchema = z.object({ npmPackageName: z.string(), addedDate: z.coerce.date(), order: z.number().optional(), + status: z.enum(['active', 'inactive', 'archived']), + staleSince: z.coerce.date().optional(), }); async function main() { @@ -71,6 +73,7 @@ async function main() { error, ); } + hasErrors = true; } } if (hasErrors) { diff --git a/yarn.lock b/yarn.lock index 662f17caa8..a8c2e6d7b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -252,7 +252,7 @@ __metadata: languageName: node linkType: hard -"@arcanis/slice-ansi@npm:^1.0.2, @arcanis/slice-ansi@npm:^1.1.1": +"@arcanis/slice-ansi@npm:^1.1.1": version: 1.1.1 resolution: "@arcanis/slice-ansi@npm:1.1.1" dependencies: @@ -261,15 +261,6 @@ __metadata: languageName: node linkType: hard -"@ardatan/sync-fetch@npm:^0.0.1": - version: 0.0.1 - resolution: "@ardatan/sync-fetch@npm:0.0.1" - dependencies: - node-fetch: "npm:^2.6.1" - checksum: 10/ee21741badecb18fb9a18a404275e25272f67ade914f98885de79ccecba3403b8a6357e6b033a028e24f0d902197dd541655309d7789ebacd7ad981bf1f12618 - languageName: node - linkType: hard - "@asamuzakjp/css-color@npm:^3.2.0": version: 3.2.0 resolution: "@asamuzakjp/css-color@npm:3.2.0" @@ -479,7 +470,7 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/util@npm:^5.2.0": +"@aws-crypto/util@npm:5.2.0, @aws-crypto/util@npm:^5.2.0": version: 5.2.0 resolution: "@aws-crypto/util@npm:5.2.0" dependencies: @@ -501,877 +492,604 @@ __metadata: linkType: hard "@aws-sdk/client-codecommit@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/client-codecommit@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/client-codecommit@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - uuid: "npm:^9.0.1" - checksum: 10/bf10987e2a0b0078cc2fa32b8339cf76fff50f2608b475c1b9f02a8eb1074bd5755e9c61802e134bc2ccacc0f965e96063ff94552e01397434460578460aebe3 + checksum: 10/f902307f0317610d5a60238e6a6491d6fca294150829a01ef2427bc6dfa508bc7382fe2071263f10ea4a97694721ebe1e1bbfb8ebe8c1a487f3d6b8c37f62eee languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/client-cognito-identity@npm:3.651.1" +"@aws-sdk/client-cognito-identity@npm:3.1005.0": + version: 3.1005.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/0934d5f991a07f8008788431762b0f13f784d6ac224b34ece0e67e6a5545f7b93a778c1b70f8b3a16601f621987fb7c3b7df1bf2c7fe360d1f11ef965781007b + checksum: 10/d3e5197002f96d12035a608e0e6f219ff179df08a4b75aa17f311191f2eec40361a904391225cb0cbd8824cc9276834705d2d9a98c041c4687792babdc334706 languageName: node linkType: hard "@aws-sdk/client-eks@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/client-eks@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/client-eks@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" - "@smithy/util-waiter": "npm:^3.1.3" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" + "@smithy/util-waiter": "npm:^4.2.11" tslib: "npm:^2.6.2" - uuid: "npm:^9.0.1" - checksum: 10/6e1d563058139d97c86649c2ff0c19c60f142b7ef556577ac30b2e5181b5c6299a408c78c6288ab17408cb2f434e1dd321b1e755415ff7fc9f24bc0bac433c00 + checksum: 10/5b76eea5ef54d11ddaf52bf366e56fd827efa6858cb4210e3d4e8b0b544bd0899889e0f4d0e29ff80daefd8ae31d1f3ea7bec0a63021da0a1264792b2056d515 languageName: node linkType: hard "@aws-sdk/client-organizations@npm:^3.350.0": - version: 3.652.0 - resolution: "@aws-sdk/client-organizations@npm:3.652.0" + version: 3.1005.0 + resolution: "@aws-sdk/client-organizations@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/1bd38da5311a376ab8bf686277155424b945f207399f263a14d5b1e8bf49fd6edb1b739308ed15483939d0840ce4987bcd44cf8c99ad36d9e00cd8656e87e40f + checksum: 10/98a900d86b5bacb102725751614caaec2c23c255336a9c4da47bfa409ea45ea548241c534bca7d34687827ca3f244144abf416ddec7ee4e1cc969d0514838c8f languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/client-s3@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/client-s3@npm:3.1005.0" dependencies: "@aws-crypto/sha1-browser": "npm:5.2.0" "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-bucket-endpoint": "npm:3.649.0" - "@aws-sdk/middleware-expect-continue": "npm:3.649.0" - "@aws-sdk/middleware-flexible-checksums": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-location-constraint": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-sdk-s3": "npm:3.651.1" - "@aws-sdk/middleware-ssec": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/signature-v4-multi-region": "npm:3.651.1" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@aws-sdk/xml-builder": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/eventstream-serde-browser": "npm:^3.0.7" - "@smithy/eventstream-serde-config-resolver": "npm:^3.0.4" - "@smithy/eventstream-serde-node": "npm:^3.0.6" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-blob-browser": "npm:^3.1.3" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/hash-stream-node": "npm:^3.1.3" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/md5-js": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-stream": "npm:^3.1.4" - "@smithy/util-utf8": "npm:^3.0.0" - "@smithy/util-waiter": "npm:^3.1.3" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-bucket-endpoint": "npm:^3.972.7" + "@aws-sdk/middleware-expect-continue": "npm:^3.972.7" + "@aws-sdk/middleware-flexible-checksums": "npm:^3.973.5" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-location-constraint": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.19" + "@aws-sdk/middleware-ssec": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/eventstream-serde-browser": "npm:^4.2.11" + "@smithy/eventstream-serde-config-resolver": "npm:^4.3.11" + "@smithy/eventstream-serde-node": "npm:^4.2.11" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-blob-browser": "npm:^4.2.12" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/hash-stream-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/md5-js": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-stream": "npm:^4.5.17" + "@smithy/util-utf8": "npm:^4.2.2" + "@smithy/util-waiter": "npm:^4.2.11" tslib: "npm:^2.6.2" - checksum: 10/3531e9db5e7f174ea46b3a5ea7d94566660173e7f8a6c8df8bbfbf5659f7a8213e9f17d8135ee65d0d98168beb4dc875ab6dff2730d138df7875226e9f0ac5eb + checksum: 10/0e57dfa15bf610ff410ab1677adb118091031e8915688ff9376620caba1f073c788422a9f762ddf53828be99591e3ec518d62bc3f550ab8babcb0a04ccb38b7f languageName: node linkType: hard "@aws-sdk/client-sesv2@npm:^3.911.0": - version: 3.946.0 - resolution: "@aws-sdk/client-sesv2@npm:3.946.0" + version: 3.1005.0 + resolution: "@aws-sdk/client-sesv2@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/credential-provider-node": "npm:3.946.0" - "@aws-sdk/middleware-host-header": "npm:3.936.0" - "@aws-sdk/middleware-logger": "npm:3.936.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.936.0" - "@aws-sdk/middleware-user-agent": "npm:3.946.0" - "@aws-sdk/region-config-resolver": "npm:3.936.0" - "@aws-sdk/signature-v4-multi-region": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/util-endpoints": "npm:3.936.0" - "@aws-sdk/util-user-agent-browser": "npm:3.936.0" - "@aws-sdk/util-user-agent-node": "npm:3.946.0" - "@smithy/config-resolver": "npm:^4.4.3" - "@smithy/core": "npm:^3.18.7" - "@smithy/fetch-http-handler": "npm:^5.3.6" - "@smithy/hash-node": "npm:^4.2.5" - "@smithy/invalid-dependency": "npm:^4.2.5" - "@smithy/middleware-content-length": "npm:^4.2.5" - "@smithy/middleware-endpoint": "npm:^4.3.14" - "@smithy/middleware-retry": "npm:^4.4.14" - "@smithy/middleware-serde": "npm:^4.2.6" - "@smithy/middleware-stack": "npm:^4.2.5" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/node-http-handler": "npm:^4.4.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.13" - "@smithy/util-defaults-mode-node": "npm:^4.2.16" - "@smithy/util-endpoints": "npm:^3.2.5" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-retry": "npm:^4.2.5" - "@smithy/util-utf8": "npm:^4.2.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/da96e92e19e3e0d1815d311820dc9ad6f35b04f19247395d82b188bafc86d53ac7dc051f84eff5cf4370f1d402d537748c262af93f45009a6fb78eb690f576ae + checksum: 10/5ea7e21976ef8abbaa356085837f858fb8f43cd0610a3643621cca13a22d93ccb1fcea8f48ff9965a0c3e7d97bfb2d78e4f16654429ed9c5a472de9bdacf8a51 languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/client-sqs@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/client-sqs@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-sdk-sqs": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/md5-js": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-sdk-sqs": "npm:^3.972.14" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/md5-js": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/3cea5f89bd41f6e77669e63268c99d951a7ee0fbd81f95bda9bdfaa0f9d631d4189a27cfa5b2a75644337458be5e2e301b207f407ad73326c1482c1afb0feff3 + checksum: 10/88b9bac7fb83b1809591ccfd644b75c1983b024aa916a523212b458a42c551d901833be387be4b43d0bb855e96f0d7e7c4af5638c1226a1ba518c92cb616632f languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/client-sso-oidc@npm:3.651.1" +"@aws-sdk/client-sts@npm:^3.350.0": + version: 3.1005.0 + resolution: "@aws-sdk/client-sts@npm:3.1005.0" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - peerDependencies: - "@aws-sdk/client-sts": ^3.651.1 - checksum: 10/6b02f72dfddffd76f111b9af5af1fa2109ea62d1d5f601be806303998f68736a63133869df9b261f115233b41cd5aa36658e234bef627186737a4cce65bbf29d + checksum: 10/8975b307be414259a43d9b76e49bf6a73a47d44fd368a12725a54dc663c6cffbee0d32a1a75d2474e85cd40cf6e7feb24e172fa90a8b4c2b738909eef55f1569 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/client-sso@npm:3.651.1" +"@aws-sdk/core@npm:^3.973.19": + version: 3.973.19 + resolution: "@aws-sdk/core@npm:3.973.19" dependencies: - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/xml-builder": "npm:^3.972.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/signature-v4": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/a04173235c46b331a22faa1f91d3bb7660f10ed20cd0e591f85c73e55056b06c91f22ec356515028efa8e2561fa3ce68d66d4a5e4c62ff48a47075965e353b9d + checksum: 10/dc60d73c8cc0fd2362bb66953c4cbdeba64f4c4690305054c132f8612432aa0db1c4727808de84649a994ad5baab901bb5ba60045abe72bafa53f269b0411370 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/client-sso@npm:3.946.0" +"@aws-sdk/crc64-nvme@npm:^3.972.4": + version: 3.972.4 + resolution: "@aws-sdk/crc64-nvme@npm:3.972.4" dependencies: - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/middleware-host-header": "npm:3.936.0" - "@aws-sdk/middleware-logger": "npm:3.936.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.936.0" - "@aws-sdk/middleware-user-agent": "npm:3.946.0" - "@aws-sdk/region-config-resolver": "npm:3.936.0" - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/util-endpoints": "npm:3.936.0" - "@aws-sdk/util-user-agent-browser": "npm:3.936.0" - "@aws-sdk/util-user-agent-node": "npm:3.946.0" - "@smithy/config-resolver": "npm:^4.4.3" - "@smithy/core": "npm:^3.18.7" - "@smithy/fetch-http-handler": "npm:^5.3.6" - "@smithy/hash-node": "npm:^4.2.5" - "@smithy/invalid-dependency": "npm:^4.2.5" - "@smithy/middleware-content-length": "npm:^4.2.5" - "@smithy/middleware-endpoint": "npm:^4.3.14" - "@smithy/middleware-retry": "npm:^4.4.14" - "@smithy/middleware-serde": "npm:^4.2.6" - "@smithy/middleware-stack": "npm:^4.2.5" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/node-http-handler": "npm:^4.4.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.13" - "@smithy/util-defaults-mode-node": "npm:^4.2.16" - "@smithy/util-endpoints": "npm:^3.2.5" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-retry": "npm:^4.2.5" - "@smithy/util-utf8": "npm:^4.2.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/02f43ce6418d22ad1d4347d62eef1dc21df6945b2e2cc52f6b266488fa9d0ac51262392a5a1cfa8e8d600ffb160d3a2e1c02406a72bda3dc3dc86af4886dcbf8 + checksum: 10/2f383f4ee7437f2709bfbf6fbf2e0b4bf72c89e093294302645051a080a58e8907034c487fd80705769813615740930bfb16e723a8c2b85c20b80366c7125dd1 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.651.1, @aws-sdk/client-sts@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/client-sts@npm:3.651.1" +"@aws-sdk/credential-provider-cognito-identity@npm:^3.972.11": + version: 3.972.11 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.972.11" dependencies: - "@aws-crypto/sha256-browser": "npm:5.2.0" - "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/client-sso-oidc": "npm:3.651.1" - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/middleware-host-header": "npm:3.649.0" - "@aws-sdk/middleware-logger": "npm:3.649.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.649.0" - "@aws-sdk/middleware-user-agent": "npm:3.649.0" - "@aws-sdk/region-config-resolver": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@aws-sdk/util-user-agent-browser": "npm:3.649.0" - "@aws-sdk/util-user-agent-node": "npm:3.649.0" - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/core": "npm:^2.4.1" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/hash-node": "npm:^3.0.4" - "@smithy/invalid-dependency": "npm:^3.0.4" - "@smithy/middleware-content-length": "npm:^3.0.6" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-body-length-node": "npm:^3.0.0" - "@smithy/util-defaults-mode-browser": "npm:^3.0.16" - "@smithy/util-defaults-mode-node": "npm:^3.0.16" - "@smithy/util-endpoints": "npm:^2.1.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/3b8feea406ce54ebed9304fde68fdb1e2cacbf19eea5335214a378a513c3e30d2fdde8262123a90e5ba9935c9c3a6d3f0fcbc02cf0771068c847297a234f4632 + checksum: 10/4d2ff4419a9bba24ffbb4154cafe5b6d756071e61e005b621dbd12fd0ffcbfe5cf2b7f3a144bf72559469cc5bd9ca5287c4386552b282614de54b9f7d5eb8399 languageName: node linkType: hard -"@aws-sdk/core@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/core@npm:3.651.1" +"@aws-sdk/credential-provider-env@npm:^3.972.17": + version: 3.972.17 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.17" dependencies: - "@smithy/core": "npm:^2.4.1" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/signature-v4": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-middleware": "npm:^3.0.4" - fast-xml-parser: "npm:4.4.1" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/d394292b8b36de1784cb05acec676ae590b9786056eaf1fdfc0fe8e82c54c26f9e1f1a83329b6d237a59177d2121f22fb3ee9a22e824bf9881c032f7eaddaa63 + checksum: 10/d9746dbfbd2ed0e5e9d89518387cc3ffde3d109ec01c530f407d3fc1415f33e159241cb0fd28a7dbda0ef5c6ef6f495c71981353f9921e8102b016db16d2e02f languageName: node linkType: hard -"@aws-sdk/core@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/core@npm:3.946.0" +"@aws-sdk/credential-provider-http@npm:^3.972.19": + version: 3.972.19 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.19" dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/xml-builder": "npm:3.930.0" - "@smithy/core": "npm:^3.18.7" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/signature-v4": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-utf8": "npm:^4.2.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-stream": "npm:^4.5.17" tslib: "npm:^2.6.2" - checksum: 10/0ce2629d50f15a16e9586060dba2bf8eda4188527b1c753b2f87b687f4f452aa6b26c811c8b364db44692321bed5a359f1bc3ee4666ffe306c568cc58e213c54 + checksum: 10/7b814efb587d356643116909f0ad4af5a52ad14849b60e224cec1616722399b20eeab2a4603bf85de4b9cf79409ac824c3cb54dc1e31e5f8502d378b5f654730 languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.651.1" +"@aws-sdk/credential-provider-ini@npm:^3.972.18": + version: 3.972.18 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.18" dependencies: - "@aws-sdk/client-cognito-identity": "npm:3.651.1" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-env": "npm:^3.972.17" + "@aws-sdk/credential-provider-http": "npm:^3.972.19" + "@aws-sdk/credential-provider-login": "npm:^3.972.18" + "@aws-sdk/credential-provider-process": "npm:^3.972.17" + "@aws-sdk/credential-provider-sso": "npm:^3.972.18" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.18" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/credential-provider-imds": "npm:^4.2.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/78706b7dcfe715bb108b772718fdf83b00cbdace667db9c3515312274cebd9d523f812e97c6a517b3d3f72866265b6aceeff54d5df510100831e044745be1906 + checksum: 10/88dc5798ef004965d9edd5d0cacd912bcaf6e872476ec58f86ed3edc5d2ab7c74c2286779b9529838232f638a3db838744f24e7bb1e6427ed3974095940472ee languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.649.0" +"@aws-sdk/credential-provider-login@npm:^3.972.18": + version: 3.972.18 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.18" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/269f5a839ec1be59bb81c1acad7133597a19c3faa8b04c2687b6e7763145c3329f9c40c390b21da1752f5bc652100ba6164cbcf30a3467c833a2867ef8916ce6 + checksum: 10/d4236574005ae303d5297f93c698257148ca16fc71643c72663a92f6ea5387fece9a91b03eb2e3359ba086554a2ea8a9ec1e1791decd45c066fdcd37c98e106b languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.946.0" +"@aws-sdk/credential-provider-node@npm:^3.350.0, @aws-sdk/credential-provider-node@npm:^3.972.19": + version: 3.972.19 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.19" dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" + "@aws-sdk/credential-provider-env": "npm:^3.972.17" + "@aws-sdk/credential-provider-http": "npm:^3.972.19" + "@aws-sdk/credential-provider-ini": "npm:^3.972.18" + "@aws-sdk/credential-provider-process": "npm:^3.972.17" + "@aws-sdk/credential-provider-sso": "npm:^3.972.18" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.18" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/credential-provider-imds": "npm:^4.2.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/743a9bf2e28607a14ed1e816f650c00f068566eaff0db88505e98d4ab4757e00919db0263d272fa46d528202f4f9d48bb0932905e5de00f790fd98963754064e + checksum: 10/514ae36e8eb534bf7b17d1f4817a8704df10ea3c169a42f4d6ab3b817abbc0ce65f82e20ab8ac50b29486a864507b7a6010019b5af931cffd52c551384498b15 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.649.0" +"@aws-sdk/credential-provider-process@npm:^3.972.17": + version: 3.972.17 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.17" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-stream": "npm:^3.1.4" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/d564fc6b2ca84bf51ad980020b63f6aaf08de573ff52de2e608031354cee24fa768549b45d5cb0a9c120158c039cf67aa0f31e97767025565dbcc283b8cbe381 + checksum: 10/37fd54e2ccbfd61629daa8dbfdffc752e4abd4ffc42cf1503a79339bf8b1d8ae594f5cb4f2183fc2ab16bb1e7b2685c4539294aec2a75090c2213dab639cea3a languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.946.0" +"@aws-sdk/credential-provider-sso@npm:^3.972.18": + version: 3.972.18 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.18" dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/fetch-http-handler": "npm:^5.3.6" - "@smithy/node-http-handler": "npm:^4.4.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-stream": "npm:^4.5.6" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/token-providers": "npm:3.1005.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/c031f544dc581aa82624be0c3427811d3c5f9527c51d6dae1056dc23371a1a747d4090a38dfcda9aefc96ed22f8bfaafa66e7c58dbee49ff5035e0746aab1bee + checksum: 10/6a5d05329f34c2b27189fcce84db784f4ab9fd8da6eae445a8444f2194dd616eaba3b8e6b4da52de300478fd636a501756b6ec5612a6877caeee837fc877c6bf languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/credential-provider-ini@npm:3.651.1" +"@aws-sdk/credential-provider-web-identity@npm:^3.972.18": + version: 3.972.18 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.18" dependencies: - "@aws-sdk/credential-provider-env": "npm:3.649.0" - "@aws-sdk/credential-provider-http": "npm:3.649.0" - "@aws-sdk/credential-provider-process": "npm:3.649.0" - "@aws-sdk/credential-provider-sso": "npm:3.651.1" - "@aws-sdk/credential-provider-web-identity": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/credential-provider-imds": "npm:^3.2.1" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - peerDependencies: - "@aws-sdk/client-sts": ^3.651.1 - checksum: 10/4abac498baec245295590e699215530662848324ba45027168b341900ea8cdc9bf74968435b7ced2161cb36fe91d30c99d2409e15a0302f977b4ebacb9b0f98c - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/credential-provider-env": "npm:3.946.0" - "@aws-sdk/credential-provider-http": "npm:3.946.0" - "@aws-sdk/credential-provider-login": "npm:3.946.0" - "@aws-sdk/credential-provider-process": "npm:3.946.0" - "@aws-sdk/credential-provider-sso": "npm:3.946.0" - "@aws-sdk/credential-provider-web-identity": "npm:3.946.0" - "@aws-sdk/nested-clients": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/credential-provider-imds": "npm:^4.2.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/796b2bea8b544469672c594da106a75a40ca3f6eb244f6c832c6eb51bce7bec6dce62deedf2bee1325f4fab6c6377afd96dfe13f8c7a243464e27a4030551158 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-login@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-login@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/nested-clients": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/c6af368cee31813f2fc33d7fe9739f5a6ca5d2501bd9030237cdec1c60d95fe08181224b179d7b6694b5293d0a1b0f4a43a487e5a0caf4f7ad425a7a7104d28e - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/credential-provider-node@npm:3.651.1" - dependencies: - "@aws-sdk/credential-provider-env": "npm:3.649.0" - "@aws-sdk/credential-provider-http": "npm:3.649.0" - "@aws-sdk/credential-provider-ini": "npm:3.651.1" - "@aws-sdk/credential-provider-process": "npm:3.649.0" - "@aws-sdk/credential-provider-sso": "npm:3.651.1" - "@aws-sdk/credential-provider-web-identity": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/credential-provider-imds": "npm:^3.2.1" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/1edcf7b2f7fe4efd0f6bd784d30bb32e3da7e04e1cecb62a43f75fb56a1f398c9155ecc58fc03e43b4f1cb45a3f23603d0b8f898ce2f994977a1481a1e806832 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:3.946.0, @aws-sdk/credential-provider-node@npm:^3.350.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.946.0" - dependencies: - "@aws-sdk/credential-provider-env": "npm:3.946.0" - "@aws-sdk/credential-provider-http": "npm:3.946.0" - "@aws-sdk/credential-provider-ini": "npm:3.946.0" - "@aws-sdk/credential-provider-process": "npm:3.946.0" - "@aws-sdk/credential-provider-sso": "npm:3.946.0" - "@aws-sdk/credential-provider-web-identity": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/credential-provider-imds": "npm:^4.2.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/60b08bef3b23af39aa59b44a7d65cfcdf708f04e17215e28fd03673a4a07d04261d8b4e6cf5b4c46b9b13c0ebc9bd999352ef839831595aa280cfa6be292ff6b - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.649.0" - dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/15e46d147ca3da269ea6e8490d7e13206ce8346f862c5601a375ce933c379804aaeab2c7a3409a21586c96309fd9c7ca72bbc6cb65b07c1ad0a805dde6460ed5 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/c6bdfc5e182ff26efcf0eaadde48b4be4e301f02bf3e229afd698f41c76675a61fe1e9173e4a0b29b65666dbeb269d4601c649a9e61c9086cb694f50bde2a0bf - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/credential-provider-sso@npm:3.651.1" - dependencies: - "@aws-sdk/client-sso": "npm:3.651.1" - "@aws-sdk/token-providers": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/bfc56118499549eac2fb6860205d52cc278a7c905b57516d90918e10a6374178db4452a4924af6b8e079ae6fa5ebbbc29668b3c50f6af24e3911e86bc3d26d0b - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.946.0" - dependencies: - "@aws-sdk/client-sso": "npm:3.946.0" - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/token-providers": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/a3a1e8bc3ebe3337e6cbff415d1a39cfb1eb7785b4d312d5de3c335a05a090e7150913bcd4c5f9b88da6e9a30bf69a48a739321646850e5650cca4b73d870d1a - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.649.0" - dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - peerDependencies: - "@aws-sdk/client-sts": ^3.649.0 - checksum: 10/cf6c0c92a701dafb39215f4c5e811b26aa71b676d1d5df0a32007eb8047acdeb965e54733418286039e096e27aabad82495ad8eade4f7584a2f70933262cc497 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/nested-clients": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/00eca35ed1df538fca1faceae1db6665ba5b22dbfd852f58d089cb211beb6e8ddecd172c3e51edd196063b40684dc1855f1a3fb4f02759652ed73eefee22268e + checksum: 10/28c461ae9ee8180ad8baacd82e4ae6f63307ec52fdabfb7f618e522c08dfb4403364516b52348497c51e6e8705f41b9cac9f645bfc2da1b60e06f642abc5481b languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/credential-providers@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/credential-providers@npm:3.1005.0" dependencies: - "@aws-sdk/client-cognito-identity": "npm:3.651.1" - "@aws-sdk/client-sso": "npm:3.651.1" - "@aws-sdk/client-sts": "npm:3.651.1" - "@aws-sdk/credential-provider-cognito-identity": "npm:3.651.1" - "@aws-sdk/credential-provider-env": "npm:3.649.0" - "@aws-sdk/credential-provider-http": "npm:3.649.0" - "@aws-sdk/credential-provider-ini": "npm:3.651.1" - "@aws-sdk/credential-provider-node": "npm:3.651.1" - "@aws-sdk/credential-provider-process": "npm:3.649.0" - "@aws-sdk/credential-provider-sso": "npm:3.651.1" - "@aws-sdk/credential-provider-web-identity": "npm:3.649.0" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/credential-provider-imds": "npm:^3.2.1" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/client-cognito-identity": "npm:3.1005.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/credential-provider-cognito-identity": "npm:^3.972.11" + "@aws-sdk/credential-provider-env": "npm:^3.972.17" + "@aws-sdk/credential-provider-http": "npm:^3.972.19" + "@aws-sdk/credential-provider-ini": "npm:^3.972.18" + "@aws-sdk/credential-provider-login": "npm:^3.972.18" + "@aws-sdk/credential-provider-node": "npm:^3.972.19" + "@aws-sdk/credential-provider-process": "npm:^3.972.17" + "@aws-sdk/credential-provider-sso": "npm:^3.972.18" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.18" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/credential-provider-imds": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/ed23f905dfb8b4c78ad4b6a8871e4e9e3b63be2b3c3304e9fabdbc2ac8edf9393a6c73f14c80fcbd51b27e4c09cfbf40626c87ccf333fb7966184613b6e32561 + checksum: 10/679075ba7c092414e50e8dc969aaa616bb2b88efbfecf6780c87242079cb5de82e6971d29c8b5df831ccc3d8ecb4edfb92f1627395668fb4067a72a0c1985d4c languageName: node linkType: hard @@ -1385,34 +1103,34 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.350.0": - version: 3.651.1 - resolution: "@aws-sdk/lib-storage@npm:3.651.1" + version: 3.1005.0 + resolution: "@aws-sdk/lib-storage@npm:3.1005.0" dependencies: - "@smithy/abort-controller": "npm:^3.1.2" - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/smithy-client": "npm:^3.3.0" + "@smithy/abort-controller": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/smithy-client": "npm:^4.12.3" buffer: "npm:5.6.0" events: "npm:3.3.0" stream-browserify: "npm:3.0.0" tslib: "npm:^2.6.2" peerDependencies: - "@aws-sdk/client-s3": ^3.651.1 - checksum: 10/935dc8968acff6e125d0cf4dd2e38e7da1e75c6e6e1b314891bcacddb752c8a60f271c153c73229925c44f7d48d4c8fbf4bdda3546e869849658a5766a1f3c2b + "@aws-sdk/client-s3": ^3.1005.0 + checksum: 10/fca5e79bdcb1055cf4a63958d494874270bdf7542d2de80e6ab362f66a374732e7b0018b5b2e24e0bb958ba2ebfcbe471fa549ce2b36b7bbdbc54b5e27b4b978 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.649.0" +"@aws-sdk/middleware-bucket-endpoint@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-arn-parser": "npm:3.568.0" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-config-provider": "npm:^3.0.0" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-arn-parser": "npm:^3.972.3" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/6d3bd04068a1b9dc77f2714895b9226f16dd1e37d9fc8f227c387b296a6743e3b2430a9d4b95ff9651a885e49ec86d3d745666bc2ba714f65cad0536b46a3290 + checksum: 10/79b40fb636f6a6274d9017f543cbbbb6fb62c5a407a7af2045faefa9879fb6c3e26a72fd4528f92dc3327c944b6ba06d63a2192b4d3899b088d14f252cb5e6b8 languageName: node linkType: hard @@ -1429,173 +1147,120 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.649.0" +"@aws-sdk/middleware-expect-continue@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/96e390d4d18aa9cafdac99bd73c606b542bac5b5180c27a6ac09cce55d407bd005626d8b9a638b49f8506af20746b0509493ba78e6168139970190a63810517f + checksum: 10/dec41ef34d0c14165a11a41fa42975f6a6e9361821491b021821e5021d966326ec4a57e2023207d011ca3a5e4e1e997780e6ded7a0e78dd6f798d6c9a9b819c1 languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.651.1" +"@aws-sdk/middleware-flexible-checksums@npm:^3.973.5": + version: 3.973.5 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.973.5" dependencies: "@aws-crypto/crc32": "npm:5.2.0" "@aws-crypto/crc32c": "npm:5.2.0" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/is-array-buffer": "npm:^3.0.0" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@aws-crypto/util": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/crc64-nvme": "npm:^3.972.4" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/is-array-buffer": "npm:^4.2.2" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-stream": "npm:^4.5.17" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/06ee0c3110472eefba88aaf37bcfe532b3aed3467bd4dbca305fc372100897ff9ecc60055e6e51fb87ec778dce0ce584a8af3ba1c2c8ccb9dc0e634929181522 + checksum: 10/add5a4b0510a67beaf91a26fc8de241917cd7cb697c7c1ce24180d9c5898aad4603582a8e1424f609f6e9073fa40f965454e3fb11f0814b5059cc619a992c95d languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.649.0" +"@aws-sdk/middleware-host-header@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-host-header@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/6b0c14c6d250e8ecb9bab57d194ff12c5ff3e95db4295c02d03f5fa62db49288110773b2b9f3ed9ffcd97809160be391553a4aefe8706616bc4ff26629536d70 + checksum: 10/0689828a16afe242d3ad37e058a3e8d0ea0e15762f1db04521cefdc497e10b7a0851662043f0f74039fdf8f0de0853fad97a30ff7b1f35f66d66f8bc1b37a900 languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.936.0" +"@aws-sdk/middleware-location-constraint@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/ce707c2402e50b227aa7e22134738ab61e03982ec375926bfa8b074deaeba5aa9296891960294d92aa5010e5446e49224a9d34f7beb55a5ed7b0ea749e49924b + checksum: 10/9ddc3a5c5764aad54cdc25e0f7b95234f4ac25273fecd41ebc05d91eebcf35773905bd00ec0329517a197479b61b5401aab70dfffe46fe10a1f3ae81faf83d71 languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.649.0" +"@aws-sdk/middleware-logger@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-logger@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/08be9ddccaf6d6cc0baa66f5d822d688388445a7a4840449f480512251239b53a38267ed1b41ef25583e4b0c363f6c1e3a2c6df797d6ca3fe4bcd3bc28bea680 + checksum: 10/16408b89fedd4e984d8f4becd765450895f564979b3c11be10b7dc567ac95a4dd5c3d2de2bee2752c0a40aaaa0391360021206402daaa201adf518fe71a609f2 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-logger@npm:3.649.0" +"@aws-sdk/middleware-recursion-detection@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/3c1909eb49460e4c0f92de78ca7fdacc7d9f56a5ece0bcce63fce3da18620368d1f6d72106e928877af603e1c438acf914681fe44637481bcfb855c28c13ebc1 + checksum: 10/8afaa4bebc73403ba36c8ddff621de60a2afe62fffc41d19e3e801ed4aea6b4daad9ee76e5d6dd99a2f2184a6c6568ce0edaf32f11f490ca8c8e749db5e0c6ee languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/middleware-logger@npm:3.936.0" +"@aws-sdk/middleware-sdk-s3@npm:^3.972.19": + version: 3.972.19 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.19" dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@smithy/types": "npm:^4.9.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-arn-parser": "npm:^3.972.3" + "@smithy/core": "npm:^3.23.9" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/signature-v4": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-stream": "npm:^4.5.17" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/277d845cdf03aca6d3af548d3751fdb598462fc9877272eadc75fa24e0d27aee267fc84a3583d4116b0fe03c703c59786e56447a88c549bc7394ec8a4ec3d9ea + checksum: 10/253a60be0856b3d7732428278a2672c699b65192202c4461287baa4842fde2c0b1c2ad20ef24cb2ce556fdffcb39485392dceb4e8dfc14546baef3ea2ac35678 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.649.0" +"@aws-sdk/middleware-sdk-sqs@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.972.14" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/20d593eedc5ddb0ebe778a05e056a152101e737fed7918889490fce4fae3b636ac0e7fe2ebd7ae70e2e7f6dfebd292072d7e46514050e49d682e43cf069e97b1 - languageName: node - linkType: hard - -"@aws-sdk/middleware-recursion-detection@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.936.0" - dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@aws/lambda-invoke-store": "npm:^0.2.0" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/55fe5db2e8ef0dfcf0e3b37ea0e3640766c44d743f327b7b4dc33d764559908a918edfa4a04a3e04c2e981164998f81a52f747979a28aeed1835a29fd6634a01 - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-s3@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.651.1" - dependencies: - "@aws-sdk/core": "npm:3.651.1" - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-arn-parser": "npm:3.568.0" - "@smithy/core": "npm:^2.4.1" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/signature-v4": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-config-provider": "npm:^3.0.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-stream": "npm:^3.1.4" - "@smithy/util-utf8": "npm:^3.0.0" - tslib: "npm:^2.6.2" - checksum: 10/d7da62619b2ec412fdde5071944d912da8291fb0cdf5d3ab4253f1601e59c5c9c1c19aa5b68a8599c38dedf0f5f3e295b63619d196d9d74fba76ce70aa7b69ec - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-s3@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/util-arn-parser": "npm:3.893.0" - "@smithy/core": "npm:^3.18.7" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/signature-v4": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-config-provider": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-stream": "npm:^4.5.6" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/97b2ae196c1edfb48eb8b33332d2864053bf7ef152fade966817f9c00f76a5941e48f2224fbbcec07bc5d3f7d3b86833e1978f4b1161480d4863b971d4477876 - languageName: node - linkType: hard - -"@aws-sdk/middleware-sdk-sqs@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.649.0" - dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-hex-encoding": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^3.0.0" - tslib: "npm:^2.6.2" - checksum: 10/49ce831e3ec72f5330ddd7a95333d76c8814e7fd9bc3bf28d25178bb577df7e9790ab7909d763ca34655ba7154f29abfc4eb035491da61636b10075bd0b9e958 + checksum: 10/6cf141714951ba8e31757ac250e850f24b9d8642ab43dcd94ec74a685611157d9555184c839855226134f565b667a9cf16870fa389fdefb2dfdc373772fd2b5b languageName: node linkType: hard @@ -1609,88 +1274,76 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.649.0" +"@aws-sdk/middleware-ssec@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/middleware-ssec@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/1a9292038d2ae005528cd2c633d083910d575c3055c8b530746c239664074b0775cb705a5b5800de19d4ed4b8af333c7f6d91eb718e93715552997c4386b0403 + checksum: 10/132d841a4cab1432bd3b6b9fc1212cdb95e5329f07c4ba8307a2a376d0649dbed01bfd61a2648cfd8142bf59c405e6af7b3637fa63358eec498894e6280b94ae languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.649.0" +"@aws-sdk/middleware-user-agent@npm:^3.972.20": + version: 3.972.20 + resolution: "@aws-sdk/middleware-user-agent@npm:3.972.20" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@aws-sdk/util-endpoints": "npm:3.649.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@smithy/core": "npm:^3.23.9" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-retry": "npm:^4.2.11" tslib: "npm:^2.6.2" - checksum: 10/4e5c428c7869729850b1f7050071b82ac38f144c61f5580d6ee241b5ad87266df9001eb335e19802d5c318e23db0c5ee40fb5c00be14855e1f141f2bb22c6ef4 + checksum: 10/e6028a242fa383f1c53151c31c6e4d4702fc82c0901c42fad7a285b51be236184549bd3075da27153f615372e12694ff6a6f40e082095d3ad246dedb3ca98e0f languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/util-endpoints": "npm:3.936.0" - "@smithy/core": "npm:^3.18.7" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/db44fc55412e6d7d1fd6dfa356b44321f157961afd830956bd040233e453243a4d60e410edb9d244972e38f365ff0dd0798a339d1814d6322a3838c0680ac950 - languageName: node - linkType: hard - -"@aws-sdk/nested-clients@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/nested-clients@npm:3.946.0" +"@aws-sdk/nested-clients@npm:^3.996.8": + version: 3.996.8 + resolution: "@aws-sdk/nested-clients@npm:3.996.8" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/middleware-host-header": "npm:3.936.0" - "@aws-sdk/middleware-logger": "npm:3.936.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.936.0" - "@aws-sdk/middleware-user-agent": "npm:3.946.0" - "@aws-sdk/region-config-resolver": "npm:3.936.0" - "@aws-sdk/types": "npm:3.936.0" - "@aws-sdk/util-endpoints": "npm:3.936.0" - "@aws-sdk/util-user-agent-browser": "npm:3.936.0" - "@aws-sdk/util-user-agent-node": "npm:3.946.0" - "@smithy/config-resolver": "npm:^4.4.3" - "@smithy/core": "npm:^3.18.7" - "@smithy/fetch-http-handler": "npm:^5.3.6" - "@smithy/hash-node": "npm:^4.2.5" - "@smithy/invalid-dependency": "npm:^4.2.5" - "@smithy/middleware-content-length": "npm:^4.2.5" - "@smithy/middleware-endpoint": "npm:^4.3.14" - "@smithy/middleware-retry": "npm:^4.4.14" - "@smithy/middleware-serde": "npm:^4.2.6" - "@smithy/middleware-stack": "npm:^4.2.5" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/node-http-handler": "npm:^4.4.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-body-length-node": "npm:^4.2.1" - "@smithy/util-defaults-mode-browser": "npm:^4.3.13" - "@smithy/util-defaults-mode-node": "npm:^4.2.16" - "@smithy/util-endpoints": "npm:^3.2.5" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-retry": "npm:^4.2.5" - "@smithy/util-utf8": "npm:^4.2.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/middleware-host-header": "npm:^3.972.7" + "@aws-sdk/middleware-logger": "npm:^3.972.7" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.7" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/region-config-resolver": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.5" + "@aws-sdk/util-endpoints": "npm:^3.996.4" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.7" + "@aws-sdk/util-user-agent-node": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/core": "npm:^3.23.9" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/hash-node": "npm:^4.2.11" + "@smithy/invalid-dependency": "npm:^4.2.11" + "@smithy/middleware-content-length": "npm:^4.2.11" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-retry": "npm:^4.4.40" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.39" + "@smithy/util-defaults-mode-node": "npm:^4.2.42" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/137089ad8cdca5e864e1643acfea6305d9723d1dd4e842fc37693374391c8ce8575b4e23757c2939530e9cf4bec2821e30e1ef18abb73d6d0d8c23880658f6cc + checksum: 10/939f062f1cd022e03276f58d181832716c02269ec6f2c8fbd01fbebe6353132132611a01c7dc8e4fe9238c5f00b1ef7ca259de2db76cfb8db7fadc13cf6b8478 languageName: node linkType: hard @@ -1738,88 +1391,45 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.649.0" +"@aws-sdk/region-config-resolver@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/region-config-resolver@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-config-provider": "npm:^3.0.0" - "@smithy/util-middleware": "npm:^3.0.4" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/f1bff1161ca1f506220e4f0d36ebbdec2e43382b21c4469cd2ad54f622e8cbd9ba385a0598a9c6a127431b1d056596cfa709f767b54fcf34c950d52778429d87 + checksum: 10/2ae6097459c793691c8a95836c681de2f01940792722a73e103b8432fd69cb387f28ad6cf6b7bd334018bf5189b956cffe162ad6a803a2db4cf4224a25c7d2e0 languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.936.0" +"@aws-sdk/signature-v4-multi-region@npm:^3.996.7": + version: 3.996.7 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.7" dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@smithy/config-resolver": "npm:^4.4.3" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/types": "npm:^4.9.0" + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.19" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/signature-v4": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/384ffaba2aacb86987768a2208d29f8495322ba4ccb8e536d2aeeea7dbc92ac35bf624b9e15dee63430113f7e341d9f155ca9f72ae96bf2436616a5705f790f2 + checksum: 10/385f1be9b392c4f9dd0bdd1baa578fe479c331503b49e42935f3cb7857c96b0d18eb0417158924f5fbfb1112f440ad017c0ccbd942221b10a11edc968007ca37 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.651.1": - version: 3.651.1 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.651.1" +"@aws-sdk/token-providers@npm:3.1005.0": + version: 3.1005.0 + resolution: "@aws-sdk/token-providers@npm:3.1005.0" dependencies: - "@aws-sdk/middleware-sdk-s3": "npm:3.651.1" - "@aws-sdk/types": "npm:3.649.0" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/signature-v4": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/core": "npm:^3.973.19" + "@aws-sdk/nested-clients": "npm:^3.996.8" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/6653b7ef4793906e5726ff4cc93ccece0e07117daca4ebf9bf22299892884d9d2879cd480ccf317b6162797ad2a120c102a77d36fa11dcc63d3e73a014942f68 - languageName: node - linkType: hard - -"@aws-sdk/signature-v4-multi-region@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.946.0" - dependencies: - "@aws-sdk/middleware-sdk-s3": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/signature-v4": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/a76e1fb93e7b86710d2aec3a6c97761681b2e5f25358b8b04fe0ccf49b0ae159647c8159305294e4d1a953b1514851782c3b35b56f00a7c41c6f8fe1c3d6834c - languageName: node - linkType: hard - -"@aws-sdk/token-providers@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/token-providers@npm:3.649.0" - dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - peerDependencies: - "@aws-sdk/client-sso-oidc": ^3.649.0 - checksum: 10/9ddbfeb1003e6ca629f385feb2e00ee7e0d90ea1047eed3e3b9a783dc0175f5fba59a836dff77ab983102df4dcd65c65f1d56797c998527daa4fc670e66a0be4 - languageName: node - linkType: hard - -"@aws-sdk/token-providers@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/token-providers@npm:3.946.0" - dependencies: - "@aws-sdk/core": "npm:3.946.0" - "@aws-sdk/nested-clients": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/8c9e60a94bda6b9152e843f45ac6a53fc91b825eaa5ae3061aec12391d1abd561184d681eb71fc0c3c84fce7ea43b53d1ab13510c21d72b345361d734b47f988 + checksum: 10/b7fb8a5b93bcaa5194e502a5e2402c492e80463d0e721647ebab9d5f3c4d66086ed8ddaa5f3e7635f98ba2718d1e6c292d0c436267916d86f7932b40040a7b65 languageName: node linkType: hard @@ -1833,23 +1443,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/types@npm:3.649.0" +"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0, @aws-sdk/types@npm:^3.973.5": + version: 3.973.5 + resolution: "@aws-sdk/types@npm:3.973.5" dependencies: - "@smithy/types": "npm:^3.4.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/9929e255909c7375c965e94e33871d38a841a9a13797b7b729d8c45b0aa91d5591d80ca18a879982a2387f12c9ce4bd5b3401182fb76046bf5f180c1e3c5e951 - languageName: node - linkType: hard - -"@aws-sdk/types@npm:3.936.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": - version: 3.936.0 - resolution: "@aws-sdk/types@npm:3.936.0" - dependencies: - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/a8d11e5c88c0006962f7fb6dd37a7cab38ee5e270ffc8046c27a19b709179e1744b173e5538599f3c2326301549fbd18ab473c7dd018f8b49cff1e9c201ccf03 + checksum: 10/915c16f7fbb2ecdaa3850f719235b41c122e53406f9dc976b224fff34c7711cd9f92e4367e8a64854e4fa0451be7924ab042f7a4175bd5272a1301d9578610bd languageName: node linkType: hard @@ -1864,21 +1464,12 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-arn-parser@npm:3.568.0": - version: 3.568.0 - resolution: "@aws-sdk/util-arn-parser@npm:3.568.0" +"@aws-sdk/util-arn-parser@npm:^3.310.0, @aws-sdk/util-arn-parser@npm:^3.972.3": + version: 3.972.3 + resolution: "@aws-sdk/util-arn-parser@npm:3.972.3" dependencies: tslib: "npm:^2.6.2" - checksum: 10/b1a7f93b4f47136ee8d71bcbbd2d5d19581007f0684aff252d3bee6b9ccc7c56e765255bb1bea847171b40cdbd2eca0fb102f24cba857d1c79c54747e8ee0855 - languageName: node - linkType: hard - -"@aws-sdk/util-arn-parser@npm:3.893.0, @aws-sdk/util-arn-parser@npm:^3.310.0": - version: 3.893.0 - resolution: "@aws-sdk/util-arn-parser@npm:3.893.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/f809777714618c63e92fd5881c9573081bc94df9c5cce53917b8e1db4efa1d44f1132f6c9179f9babc4964c648d42ebee5c1ad75641c6b336007be3df561053f + checksum: 10/140a30615c914bcb37a5bb6ff825e8b6d2bedea757c2b03a4f5abb986003683ceadc322c0ee9f9a3ba4d5925357515ed7be01ef13c56c3b0126d4e1bd7292a33 languageName: node linkType: hard @@ -1892,37 +1483,25 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/util-endpoints@npm:3.649.0" +"@aws-sdk/util-endpoints@npm:^3.996.4": + version: 3.996.4 + resolution: "@aws-sdk/util-endpoints@npm:3.996.4" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-endpoints": "npm:^2.1.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-endpoints": "npm:^3.3.2" tslib: "npm:^2.6.2" - checksum: 10/865d39a0d76d2d131246184ffe3271e647d066a947c50dbd31e19990aa71fcc8fc036ed896ef562e02cfeafe1c47169b0bf20da840ad99553c7dc853df434b12 - languageName: node - linkType: hard - -"@aws-sdk/util-endpoints@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/util-endpoints@npm:3.936.0" - dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - "@smithy/util-endpoints": "npm:^3.2.5" - tslib: "npm:^2.6.2" - checksum: 10/fd9d995cd79886df424a8aea9407d9f810ea1a28af0d1f31f63310edc5839e086732479932d470404376dd5aa5672be1210cb62b9ce3942da29c0461215b94f2 + checksum: 10/4c6b60b3a6765f0079cf1707ce190b14d50043327b9e0fa3a1d9d1a8813a327686bf05f0ea620365aca8dcabb7f23019f4593602a018d8af921bf517010c2ec8 languageName: node linkType: hard "@aws-sdk/util-locate-window@npm:^3.0.0": - version: 3.183.0 - resolution: "@aws-sdk/util-locate-window@npm:3.183.0" + version: 3.965.5 + resolution: "@aws-sdk/util-locate-window@npm:3.965.5" dependencies: - tslib: "npm:^2.3.1" - checksum: 10/6aefa7ae6aa585018d551e48b92cdacd9cd38904b62f4a604f4a45f91516c4d9351835fd6e25fbbeb21a9c110db23062f58e9a8aa204b07b6f036d3b760f8b4a + tslib: "npm:^2.6.2" + checksum: 10/66391a7f6d0c383d6bc3ea67e35b0b0164798d9acbe47271fbc676cf74e7a56690f48425a91cce70e764c53ca46619f4abc076b569d8f991c19dd7c1ac4b0a79 languageName: node linkType: hard @@ -1956,90 +1535,51 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.649.0" +"@aws-sdk/util-user-agent-browser@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.7" dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/types": "npm:^4.13.0" bowser: "npm:^2.11.0" tslib: "npm:^2.6.2" - checksum: 10/54a30c9fb023091baeae64c45c91d8aed79a82a5cf107c2ab2ee722934cc9d2d3289cca8d58210eb6aaddce3d686e4d05873d5c9cdb8559885f92d162ec7ec7d + checksum: 10/adc99dd096fb7199939600c2481a10b9f447c95c55b746b0767a41255aa4c922b3c47ac6dcd271ce79839c9a2d13f6febe19bd1348ccd36cc01bbaedae663809 languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.936.0": - version: 3.936.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.936.0" +"@aws-sdk/util-user-agent-node@npm:^3.973.5": + version: 3.973.5 + resolution: "@aws-sdk/util-user-agent-node@npm:3.973.5" dependencies: - "@aws-sdk/types": "npm:3.936.0" - "@smithy/types": "npm:^4.9.0" - bowser: "npm:^2.11.0" - tslib: "npm:^2.6.2" - checksum: 10/3b08066300dfbe202ba510d547ec4988c565e0cc103c726d452696942e5c0a66dfe2b271398ee6d46e8d3da546f094d491b74f8aa91ce3ba6553e5c6802a650e - languageName: node - linkType: hard - -"@aws-sdk/util-user-agent-node@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.649.0" - dependencies: - "@aws-sdk/types": "npm:3.649.0" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" + "@aws-sdk/middleware-user-agent": "npm:^3.972.20" + "@aws-sdk/types": "npm:^3.973.5" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: 10/24c055031299ffb250a84ffd562bd4ebcc74378a4bd1dbe43c073dc021b7c28dca16ec9758339e46d23fa8f3a0e5869ae20751147750db8a62030438cc97e96b + checksum: 10/8d1f6d5d9d731eaa7f0b1abb5ee9ff87556ff509c493231d0bf17b6f0ffe405c92c9bef006f037f173b025f527a930f9473e7d826a6579655b366b57e790a2cc languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.946.0": - version: 3.946.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.946.0" +"@aws-sdk/xml-builder@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/xml-builder@npm:3.972.10" dependencies: - "@aws-sdk/middleware-user-agent": "npm:3.946.0" - "@aws-sdk/types": "npm:3.936.0" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/types": "npm:^4.9.0" + "@smithy/types": "npm:^4.13.0" + fast-xml-parser: "npm:5.4.1" tslib: "npm:^2.6.2" - peerDependencies: - aws-crt: ">=1.0.0" - peerDependenciesMeta: - aws-crt: - optional: true - checksum: 10/7f81c001abb8ea7fc8da9f342875082fa5f0c3c069b102a3fdece8e37e9560230c0f30056cf27dd4bdc47b8a5b2cc2d127c2029bf264fdb980f77729f8cee032 + checksum: 10/5f7771ee638ce65fa8f598ee3453eac391658ee6da1b23893b42829f1d07bcacc4e9376347f4d9f0752c27ed38ecbe8bf8c029f8e43ace8040f3b9dd6efc3242 languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.649.0": - version: 3.649.0 - resolution: "@aws-sdk/xml-builder@npm:3.649.0" - dependencies: - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/b47c767169d4f440a687b0dd343c2016bd8e3ea023d3a9c64256fe472d5ffe9eccb4bb1fa42bee0693cae11ed86bd866a7a98fd62dcf65dab34926c5be82579f - languageName: node - linkType: hard - -"@aws-sdk/xml-builder@npm:3.930.0": - version: 3.930.0 - resolution: "@aws-sdk/xml-builder@npm:3.930.0" - dependencies: - "@smithy/types": "npm:^4.9.0" - fast-xml-parser: "npm:5.2.5" - tslib: "npm:^2.6.2" - checksum: 10/7956588f54e282c0b6cefaeefec6103c0cf787427f29d599330241b5349635f0cc3742df0a3aa6d1792b237178e3cfa3ee156379aa160fb8d7bd854c4608263d - languageName: node - linkType: hard - -"@aws/lambda-invoke-store@npm:^0.2.0": - version: 0.2.2 - resolution: "@aws/lambda-invoke-store@npm:0.2.2" - checksum: 10/18cd0cec90d9d865c9089218ef2220b0a7302a860c9a3f808b101386f569abc5ee11eb98a36947bed280a63308dd5df23c39e7b07fe9ac4f4ffcd0c4dce537c4 +"@aws/lambda-invoke-store@npm:^0.2.2": + version: 0.2.3 + resolution: "@aws/lambda-invoke-store@npm:0.2.3" + checksum: 10/d0efa8ca73b2d8dc0bf634525eefa1b72cda85f5d47366264849343a6f2860cfa5c52b7f766a16b78da8406bbd3ee975da3abb1dbe38183f8af95413eafeb256 languageName: node linkType: hard @@ -2866,7 +2406,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 @@ -2984,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" @@ -3016,7 +2557,7 @@ __metadata: cron: "npm:^3.0.0" express: "npm:^4.22.0" express-promise-router: "npm:^4.1.0" - express-rate-limit: "npm:^7.5.0" + express-rate-limit: "npm:^8.2.2" fs-extra: "npm:^11.2.0" get-port: "npm:^5.1.1" git-url-parse: "npm:^15.0.0" @@ -3030,7 +2571,7 @@ __metadata: lodash: "npm:^4.17.21" logform: "npm:^2.3.2" luxon: "npm:^3.0.0" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" msw: "npm:^1.0.0" mysql2: "npm:^3.0.0" node-fetch: "npm:^2.7.0" @@ -3261,6 +2802,332 @@ __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-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-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:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/proper-lockfile": "npm:^4" + cleye: "npm:^2.3.0" + cross-fetch: "npm:^4.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" + yaml: "npm:^2.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-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-build@workspace:packages/cli-module-build" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/module-federation-common": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@module-federation/enhanced": "npm:^0.21.6" + "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0" + "@rollup/plugin-commonjs": "npm:^26.0.0" + "@rollup/plugin-json": "npm:^6.0.0" + "@rollup/plugin-node-resolve": "npm:^15.0.0" + "@rollup/plugin-yaml": "npm:^4.0.0" + "@rspack/core": "npm:^1.4.11" + "@rspack/dev-server": "npm:^1.1.4" + "@rspack/plugin-react-refresh": "npm:^1.4.3" + "@swc/core": "npm:^1.15.6" + "@types/fs-extra": "npm:^11.0.0" + "@types/lodash": "npm:^4.14.151" + "@types/npm-packlist": "npm:^3.0.0" + "@types/shell-quote": "npm:^1.7.5" + 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" + cross-spawn: "npm:^7.0.3" + css-loader: "npm:^6.5.1" + ctrlc-windows: "npm:^2.1.0" + esbuild-loader: "npm:^4.0.0" + eslint-rspack-plugin: "npm:^4.2.1" + eslint-webpack-plugin: "npm:^4.2.0" + fork-ts-checker-webpack-plugin: "npm:^9.0.0" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + html-webpack-plugin: "npm:^5.6.3" + lodash: "npm:^4.17.21" + mini-css-extract-plugin: "npm:^2.4.2" + node-stdlib-browser: "npm:^1.3.1" + npm-packlist: "npm:^5.0.0" + p-queue: "npm:^6.6.2" + postcss: "npm:^8.1.0" + postcss-import: "npm:^16.1.0" + process: "npm:^0.11.10" + raw-loader: "npm:^4.0.2" + react-dev-utils: "npm:^12.0.0-next.60" + react-refresh: "npm:^0.18.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" + shell-quote: "npm:^1.8.1" + style-loader: "npm:^3.3.1" + swc-loader: "npm:^0.2.3" + tar: "npm:^7.5.6" + ts-checker-rspack-plugin: "npm:^1.1.5" + ts-morph: "npm:^24.0.0" + util: "npm:^0.12.3" + webpack: "npm:~5.105.0" + webpack-dev-server: "npm:^5.0.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-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 + jest: ^29.0.0 || ^30.0.0 + jest-environment-jsdom: "*" + jsdom: ^27.1.0 + peerDependenciesMeta: + "@jest/environment-jsdom-abstract": + optional: true + jest-environment-jsdom: + optional: true + jsdom: + 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" @@ -3272,10 +3139,21 @@ __metadata: "@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": + optional: true languageName: unknown linkType: soft @@ -3286,83 +3164,43 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" - "@backstage/catalog-model": "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/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" - "@rollup/plugin-node-resolve": "npm:^15.0.0" - "@rollup/plugin-yaml": "npm:^4.0.0" - "@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/npm-packlist": "npm:^3.0.0" - "@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/svgo": "npm:^2.6.2" - "@types/tar": "npm:^6.1.1" - "@types/terser-webpack-plugin": "npm:^5.0.4" "@types/webpack-env": "npm:^1.15.2" - "@types/webpack-sources": "npm:^3.2.3" - "@types/yarnpkg__lockfile": "npm:^1.1.4" "@typescript-eslint/eslint-plugin": "npm:^8.17.0" "@typescript-eslint/parser": "npm:^8.16.0" - "@yarnpkg/lockfile": "npm:^1.1.0" - "@yarnpkg/parsers": "npm:^3.0.0" - bfj: "npm:^8.0.0" - buffer: "npm:^6.0.3" chalk: "npm:^4.0.0" - chokidar: "npm:^3.3.1" - commander: "npm:^12.0.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" @@ -3370,102 +3208,28 @@ __metadata: 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" - lodash: "npm:^4.17.21" - mini-css-extract-plugin: "npm:^2.4.2" - minimatch: "npm:^9.0.0" - 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" - raw-loader: "npm:^4.0.2" - react-dev-utils: "npm:^12.0.0-next.60" - react-refresh: "npm:^0.17.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" - 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.104.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" - zod-validation-error: "npm:^4.0.2" 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.104.0 - webpack-dev-server: ^5.0.0 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: - optional: true - terser-webpack-plugin: - optional: true - webpack: - optional: true - webpack-dev-server: - optional: true bin: backstage-cli: bin/backstage-cli languageName: unknown @@ -3480,7 +3244,7 @@ __metadata: "@types/jscodeshift": "npm:^0.12.0" "@types/node": "npm:^22.13.14" chalk: "npm:^4.0.0" - commander: "npm:^12.0.0" + commander: "npm:^14.0.3" jscodeshift: "npm:^0.16.0" jscodeshift-add-imports: "npm:^1.0.10" bin: @@ -3537,6 +3301,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" + "@backstage/ui": "workspace:^" "@backstage/version-bridge": "workspace:^" "@testing-library/dom": "npm:^10.0.0" "@testing-library/jest-dom": "npm:^6.0.0" @@ -3579,6 +3344,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -3736,7 +3502,7 @@ __metadata: "@types/node": "npm:^22.13.14" "@types/recursive-readdir": "npm:^2.2.0" chalk: "npm:^4.0.0" - commander: "npm:^12.0.0" + commander: "npm:^14.0.3" fs-extra: "npm:^11.2.0" handlebars: "npm:^4.7.3" inquirer: "npm:^8.2.0" @@ -3822,7 +3588,7 @@ __metadata: "@manypkg/get-packages": "npm:^1.1.3" "@types/estree": "npm:^1.0.5" eslint: "npm:^8.33.0" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" languageName: unknown linkType: soft @@ -3906,6 +3672,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" @@ -4080,6 +3873,7 @@ __metadata: lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" msw: "npm:^1.0.0" + p-throttle: "npm:^4.1.1" languageName: unknown linkType: soft @@ -4153,7 +3947,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@graphiql/react": "npm:^0.23.0" + "@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" @@ -4164,9 +3958,9 @@ __metadata: "@types/highlightjs": "npm:^10.1.0" "@types/react": "npm:^18.0.0" "@types/swagger-ui-react": "npm:^5.0.0" - graphiql: "npm:3.1.1" + graphiql: "npm:^3.9.0" graphql: "npm:^16.0.0" - graphql-config: "npm:^5.0.2" + graphql-config: "npm:^5.1.6" graphql-ws: "npm:^5.4.1" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" @@ -4260,7 +4054,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" @@ -4297,6 +4091,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" + "@backstage/ui": "workspace:^" "@backstage/version-bridge": "workspace:^" "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" @@ -4767,12 +4562,14 @@ __metadata: express: "npm:^4.22.0" express-promise-router: "npm:^4.1.0" express-session: "npm:^1.17.1" + ipaddr.js: "npm:^2.3.0" jose: "npm:^5.0.0" knex: "npm:^3.0.0" lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" matcher: "npm:^4.0.0" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" + msw: "npm:^1.0.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" @@ -4846,16 +4643,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" @@ -5092,7 +4886,7 @@ __metadata: "@types/lodash": "npm:^4.14.151" git-url-parse: "npm:^15.0.0" lodash: "npm:^4.17.21" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" msw: "npm:^2.0.0" octokit: "npm:^3.0.0" type-fest: "npm:^4.41.0" @@ -5327,7 +5121,7 @@ __metadata: knex: "npm:^3.0.0" lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" msw: "npm:^2.0.0" p-limit: "npm:^3.0.2" prom-client: "npm:^15.0.0" @@ -5370,9 +5164,11 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" + "@backstage/ui": "workspace:^" "@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" @@ -5462,6 +5258,7 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" + "@opentelemetry/api": "npm:^1.9.0" lodash: "npm:^4.17.21" msw: "npm:^1.0.0" yaml: "npm:^2.0.0" @@ -6275,14 +6072,18 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/types": "workspace:^" "@cfworker/json-schema": "npm:^4.1.1" "@modelcontextprotocol/sdk": "npm:^1.25.2" "@types/express": "npm:^4.17.6" + "@types/supertest": "npm:^2.0.8" express: "npm:^4.22.0" express-promise-router: "npm:^4.1.0" + minimatch: "npm:^10.2.1" + supertest: "npm:^7.0.0" zod: "npm:^3.25.76" languageName: unknown linkType: soft @@ -6523,6 +6324,7 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" + "@backstage/ui": "workspace:^" "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:4.0.0-alpha.61" @@ -6691,7 +6493,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-azure@workspace:^, @backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure": +"@backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure" dependencies: @@ -6707,7 +6509,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:^, @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud": +"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud@workspace:plugins/scaffolder-backend-module-bitbucket-cloud" dependencies: @@ -6728,7 +6530,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:^, @backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server": +"@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket-server@workspace:plugins/scaffolder-backend-module-bitbucket-server" dependencies: @@ -6746,26 +6548,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-bitbucket@workspace:^, @backstage/plugin-scaffolder-backend-module-bitbucket@workspace:plugins/scaffolder-backend-module-bitbucket": - version: 0.0.0-use.local - resolution: "@backstage/plugin-scaffolder-backend-module-bitbucket@workspace:plugins/scaffolder-backend-module-bitbucket" - dependencies: - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/integration": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - fs-extra: "npm:^11.2.0" - msw: "npm:^1.0.0" - yaml: "npm:^2.0.0" - languageName: unknown - linkType: soft - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown" @@ -6826,7 +6608,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-gerrit@workspace:^, @backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit": +"@backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit" dependencies: @@ -6843,7 +6625,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-gitea@workspace:^, @backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea": +"@backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea" dependencies: @@ -6887,7 +6669,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:^, @backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": +"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab" dependencies: @@ -6899,8 +6681,9 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - "@gitbeaker/requester-utils": "npm:^41.2.0" - "@gitbeaker/rest": "npm:^41.2.0" + "@gitbeaker/core": "npm:^43.8.0" + "@gitbeaker/requester-utils": "npm:^43.8.0" + "@gitbeaker/rest": "npm:^43.8.0" luxon: "npm:^3.0.0" yaml: "npm:^2.0.0" zod: "npm:^3.25.76" @@ -6980,7 +6763,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend@workspace:plugins/scaffolder-backend" dependencies: - "@backstage/backend-app-api": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" @@ -6990,21 +6772,10 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" - "@backstage/plugin-auth-node": "workspace:^" - "@backstage/plugin-bitbucket-cloud-common": "workspace:^" - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-azure": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-bitbucket": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-github": "workspace:^" - "@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" @@ -7017,7 +6788,6 @@ __metadata: "@types/nunjucks": "npm:^3.1.4" "@types/supertest": "npm:^2.0.8" "@types/zen-observable": "npm:^0.8.0" - concat-stream: "npm:^2.0.0" esbuild: "npm:^0.27.0" express: "npm:^4.22.0" fs-extra: "npm:^11.2.0" @@ -7030,7 +6800,6 @@ __metadata: logform: "npm:^2.3.2" luxon: "npm:^3.0.0" nunjucks: "npm:^3.2.3" - p-limit: "npm:^3.1.0" p-queue: "npm:^6.6.2" prom-client: "npm:^15.0.0" strip-ansi: "npm:^7.1.0" @@ -7116,12 +6885,18 @@ __metadata: isomorphic-git: "npm:^1.23.0" jsonschema: "npm:^1.5.0" lodash: "npm:^4.17.21" + msw: "npm:^1.0.0" p-limit: "npm:^3.1.0" tar: "npm:^7.5.6" winston: "npm:^3.2.1" winston-transport: "npm:^4.7.0" zod: "npm:^3.25.76" zod-to-json-schema: "npm:^3.25.1" + peerDependencies: + "@backstage/backend-test-utils": "workspace:^" + peerDependenciesMeta: + "@backstage/backend-test-utils": + optional: true languageName: unknown linkType: soft @@ -7165,7 +6940,7 @@ __metadata: ajv: "npm:^8.0.1" ajv-errors: "npm:^3.0.0" classnames: "npm:^2.2.6" - flatted: "npm:3.3.3" + flatted: "npm:^3.3.4" humanize-duration: "npm:^3.25.1" immer: "npm:^9.0.6" json-schema: "npm:^0.4.0" @@ -7183,11 +6958,15 @@ __metadata: zod: "npm:^3.25.76" zod-to-json-schema: "npm:^3.25.1" peerDependencies: + "@backstage/frontend-test-utils": "workspace:^" "@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: ^6.30.3 react-router-dom: ^6.30.2 peerDependenciesMeta: + "@backstage/frontend-test-utils": + optional: true "@types/react": optional: true languageName: unknown @@ -7856,7 +7635,7 @@ __metadata: "@testing-library/user-event": "npm:^14.0.0" "@types/dompurify": "npm:^3.0.0" "@types/react": "npm:^18.0.0" - dompurify: "npm:^3.2.4" + dompurify: "npm:^3.3.2" git-url-parse: "npm:^15.0.0" lodash: "npm:^4.17.21" react: "npm:^18.0.2" @@ -7982,7 +7761,7 @@ __metadata: "@electric-sql/pglite": "npm:^0.3.0" "@manypkg/get-packages": "npm:^1.1.3" "@microsoft/api-documenter": "npm:^7.28.1" - "@microsoft/api-extractor": "npm:^7.55.1" + "@microsoft/api-extractor": "npm:^7.57.3" "@openapitools/openapi-generator-cli": "npm:^2.7.0" "@prettier/sync": "npm:^0.6.1" "@stoplight/spectral-core": "npm:^1.18.0" @@ -8000,7 +7779,7 @@ __metadata: chokidar: "npm:^3.5.3" codeowners-utils: "npm:^1.0.2" command-exists: "npm:^1.2.9" - commander: "npm:^12.0.0" + commander: "npm:^14.0.3" fs-extra: "npm:^11.2.0" glob: "npm:^8.0.3" globby: "npm:^11.0.0" @@ -8011,7 +7790,7 @@ __metadata: knex-pglite: "npm:^0.11.0" knip: "npm:^5.42.0" lodash: "npm:^4.17.21" - minimatch: "npm:^9.0.0" + minimatch: "npm:^10.2.1" p-limit: "npm:^3.0.2" portfinder: "npm:^1.0.32" tar: "npm:^7.5.6" @@ -8124,15 +7903,17 @@ __metadata: "@tanstack/react-table": "npm:^8.21.3" "@types/react": "npm:^18.0.0" "@types/react-dom": "npm:^18.0.0" + "@types/use-sync-external-store": "npm:^0.0.6" clsx: "npm:^2.1.1" eslint-plugin-storybook: "npm:^10.3.0-alpha.1" glob: "npm:^11.0.1" - globals: "npm:^15.11.0" + globals: "npm:^17.0.0" react: "npm:^18.0.2" react-aria-components: "npm:^1.14.0" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" storybook: "npm:^10.3.0-alpha.1" + use-sync-external-store: "npm:^1.4.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -8212,11 +7993,11 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^7.0.14": - version: 7.0.14 - resolution: "@changesets/apply-release-plan@npm:7.0.14" +"@changesets/apply-release-plan@npm:^7.1.0": + version: 7.1.0 + resolution: "@changesets/apply-release-plan@npm:7.1.0" dependencies: - "@changesets/config": "npm:^3.1.2" + "@changesets/config": "npm:^3.1.3" "@changesets/get-version-range-type": "npm:^0.4.0" "@changesets/git": "npm:^3.0.4" "@changesets/should-skip-package": "npm:^0.1.2" @@ -8229,7 +8010,7 @@ __metadata: prettier: "npm:^2.7.1" resolve-from: "npm:^5.0.0" semver: "npm:^7.5.3" - checksum: 10/7735783734bddd6d628e3a18c6de253685504c18f580636979fe558dda88501c8e4bda28c34a2f9da96f80fae0d1228271857d86fff6045226ce04b18d8b98b6 + checksum: 10/2ad86efb4b4218540e1ff17414436edcf7b801727dc4ec6cd1de42b5bf206e1fc0a29b14872e9635a9e991d6342ec9fb9a8b63c9b50679afd716f8bb3c1c847e languageName: node linkType: hard @@ -8257,31 +8038,29 @@ __metadata: linkType: hard "@changesets/cli@npm:^2.14.0": - version: 2.29.8 - resolution: "@changesets/cli@npm:2.29.8" + version: 2.30.0 + resolution: "@changesets/cli@npm:2.30.0" dependencies: - "@changesets/apply-release-plan": "npm:^7.0.14" + "@changesets/apply-release-plan": "npm:^7.1.0" "@changesets/assemble-release-plan": "npm:^6.0.9" "@changesets/changelog-git": "npm:^0.2.1" - "@changesets/config": "npm:^3.1.2" + "@changesets/config": "npm:^3.1.3" "@changesets/errors": "npm:^0.2.0" "@changesets/get-dependents-graph": "npm:^2.1.3" - "@changesets/get-release-plan": "npm:^4.0.14" + "@changesets/get-release-plan": "npm:^4.0.15" "@changesets/git": "npm:^3.0.4" "@changesets/logger": "npm:^0.1.1" "@changesets/pre": "npm:^2.0.2" - "@changesets/read": "npm:^0.6.6" + "@changesets/read": "npm:^0.6.7" "@changesets/should-skip-package": "npm:^0.1.2" "@changesets/types": "npm:^6.1.0" "@changesets/write": "npm:^0.4.0" "@inquirer/external-editor": "npm:^1.0.2" "@manypkg/get-packages": "npm:^1.1.3" ansi-colors: "npm:^4.1.3" - ci-info: "npm:^3.7.0" enquirer: "npm:^2.4.1" fs-extra: "npm:^7.0.1" mri: "npm:^1.2.0" - p-limit: "npm:^2.2.0" package-manager-detector: "npm:^0.2.0" picocolors: "npm:^1.1.0" resolve-from: "npm:^5.0.0" @@ -8290,22 +8069,23 @@ __metadata: term-size: "npm:^2.1.0" bin: changeset: bin.js - checksum: 10/1169d97d7d0b86fdeb778aadc1ffa3e46c840345c97b4cdbe90e5fc0168d0d0870001d66f6676537716a2d22c147a84e8a120f1298156419dc6a662681861af5 + checksum: 10/0ffd121b0349cfa3390de581905d3d7db9a650fcefe80e4cfbf9f4b55ddad53afc09ea2f2e78abd848db53479e2b54568d99e6d09d5d06f556adb45bb66aec53 languageName: node linkType: hard -"@changesets/config@npm:^3.1.2": - version: 3.1.2 - resolution: "@changesets/config@npm:3.1.2" +"@changesets/config@npm:^3.1.3": + version: 3.1.3 + resolution: "@changesets/config@npm:3.1.3" dependencies: "@changesets/errors": "npm:^0.2.0" "@changesets/get-dependents-graph": "npm:^2.1.3" "@changesets/logger": "npm:^0.1.1" + "@changesets/should-skip-package": "npm:^0.1.2" "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" fs-extra: "npm:^7.0.1" micromatch: "npm:^4.0.8" - checksum: 10/c35626240c0af83433808216be48cc39dd0b27d20a7d3bbb95c0da0044a08207986678dae97f081cc524abf8351e0303890794a28e8c67f17036bd88013b2576 + checksum: 10/278699f2b1673e07b9744fcd83948149647f55f295dc9d4bd22e9a1e80309863a58513bb1429376959b66358156b77a50a8d60eb70bf0ba9a3fab5402ccd5980 languageName: node linkType: hard @@ -8330,17 +8110,17 @@ __metadata: languageName: node linkType: hard -"@changesets/get-release-plan@npm:^4.0.14": - version: 4.0.14 - resolution: "@changesets/get-release-plan@npm:4.0.14" +"@changesets/get-release-plan@npm:^4.0.15": + version: 4.0.15 + resolution: "@changesets/get-release-plan@npm:4.0.15" dependencies: "@changesets/assemble-release-plan": "npm:^6.0.9" - "@changesets/config": "npm:^3.1.2" + "@changesets/config": "npm:^3.1.3" "@changesets/pre": "npm:^2.0.2" - "@changesets/read": "npm:^0.6.6" + "@changesets/read": "npm:^0.6.7" "@changesets/types": "npm:^6.1.0" "@manypkg/get-packages": "npm:^1.1.3" - checksum: 10/0b54f4e34dc27aa9df928488bf84f3d6a2b516701985d06b49306d45b87b48e642aef3de751f9517de4c1b88011b5826975aa85f8ba596da1f9681a5d1699093 + checksum: 10/eba3de04c03131604ab717af66c5d645e1fe4823034864e28eccf99fb64fbeb4cf2c4c11b8f0e7ef78a916f96eeaebdf4569df583cbfe0f3928bb0195baf27c7 languageName: node linkType: hard @@ -8373,13 +8153,13 @@ __metadata: languageName: node linkType: hard -"@changesets/parse@npm:^0.4.2": - version: 0.4.2 - resolution: "@changesets/parse@npm:0.4.2" +"@changesets/parse@npm:^0.4.3": + version: 0.4.3 + resolution: "@changesets/parse@npm:0.4.3" dependencies: "@changesets/types": "npm:^6.1.0" js-yaml: "npm:^4.1.1" - checksum: 10/d45d7f5d7a0aeede197935f16bb459479c8d0b16ebe89ceaf4bd58b307ef1be696bcc5d5fc33d5b64a80dec946b49f6107af32d57d91967e6b3f9013a0d53740 + checksum: 10/f5742266a4ecb90a8283868f2bec740ce7be94745dee7df0b2f47882775d700bdfa3b660c2ec8d06b64153cf9b02cb3d46064f391c62933c9c60a9570763f928 languageName: node linkType: hard @@ -8395,18 +8175,18 @@ __metadata: languageName: node linkType: hard -"@changesets/read@npm:^0.6.6": - version: 0.6.6 - resolution: "@changesets/read@npm:0.6.6" +"@changesets/read@npm:^0.6.7": + version: 0.6.7 + resolution: "@changesets/read@npm:0.6.7" dependencies: "@changesets/git": "npm:^3.0.4" "@changesets/logger": "npm:^0.1.1" - "@changesets/parse": "npm:^0.4.2" + "@changesets/parse": "npm:^0.4.3" "@changesets/types": "npm:^6.1.0" fs-extra: "npm:^7.0.1" p-filter: "npm:^2.1.0" picocolors: "npm:^1.1.0" - checksum: 10/3ac0cf24159b0e0fea4339d0a01c57459a6b7796f868dca7db65727c3dd33ead38b78f224b677cf7b50bb7b96fa3d0b155843e800a524b435a772c9ed21fa914 + checksum: 10/6bf3fd4b0c743d2ef53cb39c4e52731622949a695f031412ff6ea9f30860620a1d9deb1cfd1af0c92d2e2d275b6d949c0cc1e83c192f2094e0071d690c48a42e languageName: node linkType: hard @@ -8476,8 +8256,8 @@ __metadata: linkType: hard "@codemirror/language@npm:^6.0.0": - version: 6.12.1 - resolution: "@codemirror/language@npm:6.12.1" + version: 6.12.2 + resolution: "@codemirror/language@npm:6.12.2" dependencies: "@codemirror/state": "npm:^6.0.0" "@codemirror/view": "npm:^6.23.0" @@ -8485,7 +8265,7 @@ __metadata: "@lezer/highlight": "npm:^1.0.0" "@lezer/lr": "npm:^1.0.0" style-mod: "npm:^4.0.0" - checksum: 10/a24c3512d38cbb2a20cc3128da0eea074b4a6102b6a5a041b3dfd5e67638fb61dcdf4743ed87708db882df5d72a84d9f891aac6fa68447830989c8e2d9ffa2ba + checksum: 10/9afc704e17cad4782b68ad02869503a27103c4c3ac6927d2ffa3c6decfad627ade40b0ed78d1d6e941b2e879a7a79bda0d573e58604f3923f9e1f3311108ec36 languageName: node linkType: hard @@ -8542,14 +8322,14 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.23.0": - version: 6.39.14 - resolution: "@codemirror/view@npm:6.39.14" + version: 6.39.16 + resolution: "@codemirror/view@npm:6.39.16" dependencies: "@codemirror/state": "npm:^6.5.0" crelt: "npm:^1.0.6" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/956e79758d97e9fc6f6fd9a21575148cb0126712e0a3f78dc03031ebed1a5787eae581eda140f1af6686a5822b3c9c3a67547d2421a16003d45a2c0b6f83ca5e + checksum: 10/199576febda2a91fe7676b8708627ed2e38d7e964ec8258331422fe7c7f89003eee2de7dec828e09c046de005742fd476cae6ceebc7bd994744f771253bfcbf3 languageName: node linkType: hard @@ -8939,6 +8719,38 @@ __metadata: languageName: node linkType: hard +"@envelop/core@npm:^5.4.0": + version: 5.5.1 + resolution: "@envelop/core@npm:5.5.1" + dependencies: + "@envelop/instrumentation": "npm:^1.0.0" + "@envelop/types": "npm:^5.2.1" + "@whatwg-node/promise-helpers": "npm:^1.2.4" + tslib: "npm:^2.5.0" + checksum: 10/1dfac266702047d07533a893a33010fa31c5056c87196db2fcd5d44b0856d50d3410ac8aeb8f3c1b65f3a227f5fb7d1f1782051e71a9197978defe51ebcc7366 + languageName: node + linkType: hard + +"@envelop/instrumentation@npm:^1.0.0": + version: 1.0.0 + resolution: "@envelop/instrumentation@npm:1.0.0" + dependencies: + "@whatwg-node/promise-helpers": "npm:^1.2.1" + tslib: "npm:^2.5.0" + checksum: 10/4e3c9670c17e7fcf4a5654d145c64ba905abca7e71c8d8fae1ad88b1eb4a68f3a2a0aff4efd41e3d997015e5e763a7c1c335826c2b7b5f9dc691e59cd4ea472e + languageName: node + linkType: hard + +"@envelop/types@npm:^5.2.1": + version: 5.2.1 + resolution: "@envelop/types@npm:5.2.1" + dependencies: + "@whatwg-node/promise-helpers": "npm:^1.0.0" + tslib: "npm:^2.5.0" + checksum: 10/dc320a53dab896cef43de99ff972cf35da2671a248cd94fe6ab45f96954c9e505dd141cb8a3afb5fbab3d41bf4d22d30d823effb9a6fec0e7c3bb95d4c3726d1 + languageName: node + linkType: hard + "@epic-web/invariant@npm:^1.0.0": version: 1.0.0 resolution: "@epic-web/invariant@npm:1.0.0" @@ -9203,6 +9015,13 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^3.1.1": + version: 3.2.0 + resolution: "@fastify/busboy@npm:3.2.0" + checksum: 10/7d42b23eed18b1aaf2d2b1c77a5b76ec3606d59f5ddec31c04b144536be57478ed0c73e04768f11535b11a37b48aaaa0aed4904d5f18391ff90045c258e41acc + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.6.0": version: 1.6.8 resolution: "@floating-ui/core@npm:1.6.8" @@ -9323,14 +9142,14 @@ __metadata: languageName: node linkType: hard -"@gitbeaker/core@npm:^41.3.0": - version: 41.3.0 - resolution: "@gitbeaker/core@npm:41.3.0" +"@gitbeaker/core@npm:^43.8.0": + version: 43.8.0 + resolution: "@gitbeaker/core@npm:43.8.0" dependencies: - "@gitbeaker/requester-utils": "npm:^41.3.0" - qs: "npm:^6.12.2" + "@gitbeaker/requester-utils": "npm:^43.8.0" + qs: "npm:^6.14.0" xcase: "npm:^2.0.1" - checksum: 10/db60bfedcd541bd9826f7292630109934a4c667c240e4e914cdb8034c7628a841ebef8701f3d8ed5ac7a64b67479ae7415049a45071b12e40787332653f0f99f + checksum: 10/1c935ffec246c854d7d543cc4ccb97271c322ea506c02ccf60774bffd42462c36cd3324d6876082da706d25e444878f76714746b1fbd77c202b0e3766ef5f4ae languageName: node linkType: hard @@ -9346,15 +9165,15 @@ __metadata: languageName: node linkType: hard -"@gitbeaker/requester-utils@npm:^41.2.0, @gitbeaker/requester-utils@npm:^41.3.0": - version: 41.3.0 - resolution: "@gitbeaker/requester-utils@npm:41.3.0" +"@gitbeaker/requester-utils@npm:^43.8.0": + version: 43.8.0 + resolution: "@gitbeaker/requester-utils@npm:43.8.0" dependencies: picomatch-browser: "npm:^2.2.6" - qs: "npm:^6.12.2" - rate-limiter-flexible: "npm:^4.0.1" + qs: "npm:^6.14.0" + rate-limiter-flexible: "npm:^8.0.1" xcase: "npm:^2.0.1" - checksum: 10/10e3d6a87368f7111493a86f17dc969f0e8abb1aae07ac577b204acc297efa1711bfb4d6ba1f8299273e2bc410a755bd329111943fc6337c573cdbb38ab5f899 + checksum: 10/529685994676068a18f4bf6329252c66a62c1ef160f6e9d4b3a916330189cc1628fb30c983625d38295cef2999438e14ecb611fcd8a484b7c479b6dcf29f7679 languageName: node linkType: hard @@ -9368,25 +9187,25 @@ __metadata: languageName: node linkType: hard -"@gitbeaker/rest@npm:^41.2.0": - version: 41.3.0 - resolution: "@gitbeaker/rest@npm:41.3.0" +"@gitbeaker/rest@npm:^43.8.0": + version: 43.8.0 + resolution: "@gitbeaker/rest@npm:43.8.0" dependencies: - "@gitbeaker/core": "npm:^41.3.0" - "@gitbeaker/requester-utils": "npm:^41.3.0" - checksum: 10/8f280919303e2a57f9cf7070cc3bb74966be73c24448ab6cdd59c7ca6ad9f79f34a1454fb47138fddf91cc743ba769ead42513e5efb06e2be9d40f1f92432cc7 + "@gitbeaker/core": "npm:^43.8.0" + "@gitbeaker/requester-utils": "npm:^43.8.0" + checksum: 10/a29493ee1e0e789a0fa4307108c616bb287f33a390262a57025461140a284ae1284918ff4d4e91d3351e61b050eea55ffe2ca7551ffcb6be29ae22c67136a463 languageName: node linkType: hard "@google-cloud/cloud-sql-connector@npm:^1.4.0": - version: 1.9.0 - resolution: "@google-cloud/cloud-sql-connector@npm:1.9.0" + version: 1.9.1 + resolution: "@google-cloud/cloud-sql-connector@npm:1.9.1" dependencies: - "@googleapis/sqladmin": "npm:^35.0.0" + "@googleapis/sqladmin": "npm:^35.2.0" gaxios: "npm:^7.1.3" google-auth-library: "npm:^10.5.0" p-throttle: "npm:^7.0.0" - checksum: 10/30a1539a2d59ae78ef2ce275e047981dc548359d153f29715741183c5df0411da882372a020dd97be2ceb7ad6153026a14e9751b3314582f57ff7d18987c6812 + checksum: 10/52fb46d3054896ede1ec83e5361062edb96005400b8544b95c2e18d3f769feea86a56ca621620fed73938bf0b50e96bd20bb1a7fd0f15772ef2a135a2d81b6d6 languageName: node linkType: hard @@ -9488,20 +9307,20 @@ __metadata: languageName: node linkType: hard -"@googleapis/sqladmin@npm:^35.0.0": - version: 35.0.0 - resolution: "@googleapis/sqladmin@npm:35.0.0" +"@googleapis/sqladmin@npm:^35.2.0": + version: 35.2.0 + resolution: "@googleapis/sqladmin@npm:35.2.0" dependencies: googleapis-common: "npm:^8.0.0" - checksum: 10/7ecae655921c3b1e71b0b9ff04271e6ba80a6c2d4a269770c0cff6b45397abdf6a3084daba70d2659a241bcb124d0701e66ecfab2f838933582b408000aedbb0 + checksum: 10/f5931b91626a29173732381ffa7ec3c674397d3192b30da8f4b3639fef3cd88bfa3b21f7e5aa084cab1d4f90bef6860617d2d1f68e1c3692fa1c5835fc89c25a languageName: node linkType: hard -"@graphiql/react@npm:^0.20.3": - version: 0.20.3 - resolution: "@graphiql/react@npm:0.20.3" +"@graphiql/react@npm:0.29.0, @graphiql/react@npm:^0.29.0": + version: 0.29.0 + resolution: "@graphiql/react@npm:0.29.0" dependencies: - "@graphiql/toolkit": "npm:^0.9.1" + "@graphiql/toolkit": "npm:^0.11.2" "@headlessui/react": "npm:^1.7.15" "@radix-ui/react-dialog": "npm:^1.0.4" "@radix-ui/react-dropdown-menu": "npm:^2.0.5" @@ -9510,209 +9329,210 @@ __metadata: "@types/codemirror": "npm:^5.60.8" clsx: "npm:^1.2.1" codemirror: "npm:^5.65.3" - codemirror-graphql: "npm:^2.0.10" + codemirror-graphql: "npm:^2.2.1" copy-to-clipboard: "npm:^3.2.0" framer-motion: "npm:^6.5.1" - graphql-language-service: "npm:^5.2.0" - markdown-it: "npm:^12.2.0" - set-value: "npm:^4.1.0" - peerDependencies: - graphql: ^15.5.0 || ^16.0.0 - react: ^16.8.0 || ^17 || ^18 - react-dom: ^16.8.0 || ^17 || ^18 - checksum: 10/cc9bda25eaf977544c2ab9db0616d80e1e448239063c4a84aab4234de192b5fe6630a0ac2614c9f1c79ae4f42612b52ba1523cd91c98f6f21e1e01936e1fb819 - languageName: node - linkType: hard - -"@graphiql/react@npm:^0.23.0": - version: 0.23.0 - resolution: "@graphiql/react@npm:0.23.0" - dependencies: - "@graphiql/toolkit": "npm:^0.9.2" - "@headlessui/react": "npm:^1.7.15" - "@radix-ui/react-dialog": "npm:^1.0.4" - "@radix-ui/react-dropdown-menu": "npm:^2.0.5" - "@radix-ui/react-tooltip": "npm:^1.0.6" - "@radix-ui/react-visually-hidden": "npm:^1.0.3" - "@types/codemirror": "npm:^5.60.8" - clsx: "npm:^1.2.1" - codemirror: "npm:^5.65.3" - codemirror-graphql: "npm:^2.0.13" - copy-to-clipboard: "npm:^3.2.0" - framer-motion: "npm:^6.5.1" - graphql-language-service: "npm:^5.2.2" + get-value: "npm:^3.0.1" + graphql-language-service: "npm:^5.3.1" markdown-it: "npm:^14.1.0" + react-compiler-runtime: "npm:19.1.0-rc.1" set-value: "npm:^4.1.0" peerDependencies: - graphql: ^15.5.0 || ^16.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 - checksum: 10/d3e10e5e61cdc766b4b2c3597c26595c234974383e0942112174d5782bd5da3ac1be8091eea5dc7a91f475ba84f7f559f6eb36ebca87abb2d134f2e8f06a0356 + checksum: 10/c906bf3175ae042aaa035872015ea5d60991dba633e2e44f707faee9dad9f3b2b6896ca6fcef7f54e65a1b707db9a44538137e5e7037e132d0f8a456df641c49 languageName: node linkType: hard -"@graphiql/toolkit@npm:^0.9.1, @graphiql/toolkit@npm:^0.9.2": - version: 0.9.2 - resolution: "@graphiql/toolkit@npm:0.9.2" +"@graphiql/toolkit@npm:^0.11.2": + version: 0.11.3 + resolution: "@graphiql/toolkit@npm:0.11.3" dependencies: "@n1ru4l/push-pull-async-iterable-iterator": "npm:^3.1.0" meros: "npm:^1.1.4" peerDependencies: - graphql: ^15.5.0 || ^16.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 graphql-ws: ">= 4.5.0" peerDependenciesMeta: graphql-ws: optional: true - checksum: 10/cecf99652acbe3f263a08c2d895a6de99065308428458b051cb8d09e8303af24d5c62d9d6be3cc2ffa88b81f49a825696be8bd019eacb5693217c0018f812de1 + checksum: 10/a28d5261900bff00ca550963f9b0fba035e63b87dbed5c4c1fd1dd5a8963d2c614a9161af58fd4782027b86874be2403cb3e86514567e25dce171ba9823ddc62 languageName: node linkType: hard -"@graphql-tools/batch-execute@npm:^9.0.1": - version: 9.0.2 - resolution: "@graphql-tools/batch-execute@npm:9.0.2" +"@graphql-hive/signal@npm:^2.0.0": + version: 2.0.0 + resolution: "@graphql-hive/signal@npm:2.0.0" + checksum: 10/51b1b3527300e9d67366ead3c505a1abeabbf4cf764b125c21c11bb163201b2432e4c1af49ff94b13737c30e62c84a43558fd3c4f8b9b9d9e530be08a2a6f5fc + languageName: node + linkType: hard + +"@graphql-tools/batch-execute@npm:^10.0.5": + version: 10.0.5 + resolution: "@graphql-tools/batch-execute@npm:10.0.5" dependencies: - "@graphql-tools/utils": "npm:^10.0.5" - dataloader: "npm:^2.2.2" - tslib: "npm:^2.4.0" - value-or-promise: "npm:^1.0.12" + "@graphql-tools/utils": "npm:^11.0.0" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + dataloader: "npm:^2.2.3" + tslib: "npm:^2.8.1" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/ec0be4f8790c6041b4d8e796b662ce3f6ffde304ee780bf91db1912da49bed096db0e7a3b62a797920997e38d4489cd7e2ed4ee26d605a3c343ee51309ebb736 + checksum: 10/4384b095b636c4ef0cb32f4145856aa1378e92e4b892fd4bbc9ce612626806b89112482126f0fe1d89175e9954d786ec3b446a023996c4fa0c0de732735eabe0 languageName: node linkType: hard -"@graphql-tools/delegate@npm:^10.0.0, @graphql-tools/delegate@npm:^10.0.3": - version: 10.0.3 - resolution: "@graphql-tools/delegate@npm:10.0.3" +"@graphql-tools/delegate@npm:^12.0.8": + version: 12.0.8 + resolution: "@graphql-tools/delegate@npm:12.0.8" dependencies: - "@graphql-tools/batch-execute": "npm:^9.0.1" - "@graphql-tools/executor": "npm:^1.0.0" - "@graphql-tools/schema": "npm:^10.0.0" - "@graphql-tools/utils": "npm:^10.0.5" - dataloader: "npm:^2.2.2" - tslib: "npm:^2.5.0" + "@graphql-tools/batch-execute": "npm:^10.0.5" + "@graphql-tools/executor": "npm:^1.4.13" + "@graphql-tools/schema": "npm:^10.0.29" + "@graphql-tools/utils": "npm:^11.0.0" + "@repeaterjs/repeater": "npm:^3.0.6" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + dataloader: "npm:^2.2.3" + tslib: "npm:^2.8.1" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/d19e15d4bdfd1878c3f18ec6f325e62872765ef5bbbf59f074e898f8967600ae7a39b75284d69c67f1de73bcfe3a5fdb63c6a130cb1680b6277c8794c9e27ab7 + checksum: 10/c747ef3cc14141a67f275e21f8ddcbd924972670c2b50b16938ef3f3014f42aee1340973428515e62d063282083b88b8a5ff3ff9a9f459558f4f5184a5ad342c languageName: node linkType: hard -"@graphql-tools/executor-graphql-ws@npm:^1.0.0": - version: 1.1.1 - resolution: "@graphql-tools/executor-graphql-ws@npm:1.1.1" +"@graphql-tools/executor-common@npm:^1.0.6": + version: 1.0.6 + resolution: "@graphql-tools/executor-common@npm:1.0.6" dependencies: - "@graphql-tools/utils": "npm:^10.0.2" - "@types/ws": "npm:^8.0.0" - graphql-ws: "npm:^5.14.0" - isomorphic-ws: "npm:^5.0.0" - tslib: "npm:^2.4.0" - ws: "npm:^8.13.0" + "@envelop/core": "npm:^5.4.0" + "@graphql-tools/utils": "npm:^11.0.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/30d29e2ef8fbedf07d7c279218f31a7279e714328f6c24d28ea76536fb4c5ed857ab5e486922000fcf9f85b83a9f3e995b8fd066b01ea4ab31d35efaa770c133 + checksum: 10/9daf04a3d1d139d27ed8957a7166aba01a9df6c9d0a459229493c69415726afa1901e234b1bb946144a4794ba67fc4740dd791eddee1117a9c5af1d0c4423007 languageName: node linkType: hard -"@graphql-tools/executor-http@npm:^1.0.0": - version: 1.0.3 - resolution: "@graphql-tools/executor-http@npm:1.0.3" +"@graphql-tools/executor-graphql-ws@npm:^3.1.4": + version: 3.1.4 + resolution: "@graphql-tools/executor-graphql-ws@npm:3.1.4" dependencies: - "@graphql-tools/utils": "npm:^10.0.2" + "@graphql-tools/executor-common": "npm:^1.0.6" + "@graphql-tools/utils": "npm:^11.0.0" + "@whatwg-node/disposablestack": "npm:^0.0.6" + graphql-ws: "npm:^6.0.6" + isows: "npm:^1.0.7" + tslib: "npm:^2.8.1" + ws: "npm:^8.18.3" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/c5b1576e4ceb928e8d47b2c9a25585fb575dcc76fbe1cabb0e743b89361367a10858e84daddb5f742c19ae44bd69a53c8d96809bc38a8147fab18bbc66b4a067 + languageName: node + linkType: hard + +"@graphql-tools/executor-http@npm:^3.1.0": + version: 3.1.0 + resolution: "@graphql-tools/executor-http@npm:3.1.0" + dependencies: + "@graphql-hive/signal": "npm:^2.0.0" + "@graphql-tools/executor-common": "npm:^1.0.6" + "@graphql-tools/utils": "npm:^11.0.0" "@repeaterjs/repeater": "npm:^3.0.4" - "@whatwg-node/fetch": "npm:^0.9.0" - extract-files: "npm:^11.0.0" - meros: "npm:^1.2.1" - tslib: "npm:^2.4.0" - value-or-promise: "npm:^1.0.12" + "@whatwg-node/disposablestack": "npm:^0.0.6" + "@whatwg-node/fetch": "npm:^0.10.13" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + meros: "npm:^1.3.2" + tslib: "npm:^2.8.1" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/0fda267df3ebe3afd44cb86e779fcab17ab9d8045670b15ceb1b06ab10a576686beb0aa786781dae29e5bbe6ab04259e73e0fe5de2a45853fda6a19f122a12f5 + checksum: 10/6e288c0c0d57ebc217860a085e98361a527997b6a6f2d96d4d4881ae9951e94720c065d34c7ed10b6ec9ea5b632fcd32e015f1b12349e7720ae15874baf678e1 languageName: node linkType: hard -"@graphql-tools/executor-legacy-ws@npm:^1.0.0": - version: 1.1.0 - resolution: "@graphql-tools/executor-legacy-ws@npm:1.1.0" +"@graphql-tools/executor-legacy-ws@npm:^1.1.25": + version: 1.1.25 + resolution: "@graphql-tools/executor-legacy-ws@npm:1.1.25" dependencies: - "@graphql-tools/utils": "npm:^10.3.0" + "@graphql-tools/utils": "npm:^11.0.0" "@types/ws": "npm:^8.0.0" isomorphic-ws: "npm:^5.0.0" tslib: "npm:^2.4.0" - ws: "npm:^8.17.1" + ws: "npm:^8.19.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/b7bfe6c1e2229beef35fb175102d0a5ab45e679834c45d579c3d0efe8b7d1e22836d3773e03b0c920bbc94a58d490d54d62f1484e043eef6a54cc426b4b92f98 + checksum: 10/3ebcd004e6583c3f2e078e3caee7a51b707d125c0993f8a4529aa6e2a6b1aaad46b8eee16e14fde135dbaa1ec3783bc4257a1fd9466673a37e7e66a05a7fbf20 languageName: node linkType: hard -"@graphql-tools/executor@npm:^1.0.0": - version: 1.2.0 - resolution: "@graphql-tools/executor@npm:1.2.0" +"@graphql-tools/executor@npm:^1.4.13": + version: 1.5.1 + resolution: "@graphql-tools/executor@npm:1.5.1" dependencies: - "@graphql-tools/utils": "npm:^10.0.0" - "@graphql-typed-document-node/core": "npm:3.2.0" + "@graphql-tools/utils": "npm:^11.0.0" + "@graphql-typed-document-node/core": "npm:^3.2.0" "@repeaterjs/repeater": "npm:^3.0.4" + "@whatwg-node/disposablestack": "npm:^0.0.6" + "@whatwg-node/promise-helpers": "npm:^1.0.0" tslib: "npm:^2.4.0" - value-or-promise: "npm:^1.0.12" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/a13c9fb2b1e15661c469a3f048f19f96191e802842e48a1ba058a2422c30f3de64b5089f57f81ef38d49ed2261f7e72f5e34834cc322188c33306d2ae711a00d + checksum: 10/b73e4ba7b19c2ebf0d5943006626388575fa023400c2b17e96e5fe0b321dabbbc116fcf2e951a3b05398dcd078805e28d12821ddce89c088bb52c6a38c2dd2f7 languageName: node linkType: hard "@graphql-tools/graphql-file-loader@npm:^8.0.0": - version: 8.0.0 - resolution: "@graphql-tools/graphql-file-loader@npm:8.0.0" + version: 8.1.10 + resolution: "@graphql-tools/graphql-file-loader@npm:8.1.10" dependencies: - "@graphql-tools/import": "npm:7.0.0" - "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-tools/import": "npm:7.1.10" + "@graphql-tools/utils": "npm:^11.0.0" globby: "npm:^11.0.3" tslib: "npm:^2.4.0" unixify: "npm:^1.0.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/bf1248593123f6aa740da8b58746e2a60f5a1f413da1dcff8890daae0f2eeeac1837a2d419bdbdfb6ccb2877e03103d335ae0d1696e392f6af247414b0ad8406 + checksum: 10/200d48bed7c3a3320f4193e3258e5d9bdacd578e0db87d914d0a9f16b7b8884e5089975382d6c1d6e6244ddef42c53528fee0325f865bcf63dbd5269b25e4cae languageName: node linkType: hard -"@graphql-tools/import@npm:7.0.0": - version: 7.0.0 - resolution: "@graphql-tools/import@npm:7.0.0" +"@graphql-tools/import@npm:7.1.10": + version: 7.1.10 + resolution: "@graphql-tools/import@npm:7.1.10" dependencies: - "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-tools/utils": "npm:^11.0.0" + "@theguild/federation-composition": "npm:^0.21.3" resolve-from: "npm:5.0.0" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/74741f670fb028526c363cd83871eeb9a1f51ecae27d1640914b0d5ddc482dc0a74d96b996244c726a12e80f63a4f8ec15fc71098e3b87ed3c463fa06ce8ac6c + checksum: 10/4a97c7740395ca398cf537d12b3f6a1b1784cda2d7dcc42f4f3c98a9ee6d965125cc2e500add2f9e25392444896719d16f9b05f0572f2e0c54ef8f218b86f038 languageName: node linkType: hard "@graphql-tools/json-file-loader@npm:^8.0.0": - version: 8.0.0 - resolution: "@graphql-tools/json-file-loader@npm:8.0.0" + version: 8.0.26 + resolution: "@graphql-tools/json-file-loader@npm:8.0.26" dependencies: - "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-tools/utils": "npm:^11.0.0" globby: "npm:^11.0.3" tslib: "npm:^2.4.0" unixify: "npm:^1.0.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/a023466e261599803d1f8e1af3bb7b0007a5206c29df4fb14a448c1dacc04807482b97374c2bbb82bd286523f6a032c355d74f39bffb866325651f1a0f0412a2 + checksum: 10/6e4dc44a9aa3cdd3f7b958b50d98a26e56fb5cc9fab2667011ec12c82685016f269350d6ba2e45dcc7e6d71f8df7d1c4002edb55ce8896ba3e705cad14645ac1 languageName: node linkType: hard "@graphql-tools/load@npm:^8.1.0": - version: 8.1.0 - resolution: "@graphql-tools/load@npm:8.1.0" + version: 8.1.8 + resolution: "@graphql-tools/load@npm:8.1.8" dependencies: - "@graphql-tools/schema": "npm:^10.0.23" - "@graphql-tools/utils": "npm:^10.8.6" + "@graphql-tools/schema": "npm:^10.0.31" + "@graphql-tools/utils": "npm:^11.0.0" p-limit: "npm:3.1.0" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/4601dda7eb32cb8afed2379102ad82f8a948e478f42c7b1f354a3468ca8dfcdcc2a89e6c6ebcbb574c77eaa80d47f20c27230bdcd6c2d0a3600fa1d6a450cc95 + checksum: 10/a9b24c8d9fc52ebf2b5e0d5dc99212e61704cfe0a07b17a5be3329c391ae01f710e0a2ca6b73b41379e9bee55210c0466d5dfb378a9e3cbe051062a69a07d616 languageName: node linkType: hard @@ -9728,28 +9548,28 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/merge@npm:^9.0.0, @graphql-tools/merge@npm:^9.0.24": - version: 9.0.24 - resolution: "@graphql-tools/merge@npm:9.0.24" +"@graphql-tools/merge@npm:^9.0.0, @graphql-tools/merge@npm:^9.1.7": + version: 9.1.7 + resolution: "@graphql-tools/merge@npm:9.1.7" dependencies: - "@graphql-tools/utils": "npm:^10.8.6" + "@graphql-tools/utils": "npm:^11.0.0" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/95f77ff141f10d5d726cd8d1ae1ad84ed944c84346bf20461adca9b1543bb94cb524b0347885fe61d3158ccf5ffe1dddec361787ae40bfcc3449aad51528dd77 + checksum: 10/e0b77dfc16e91d7c2450df0b57a85a93e11f0f67e37e396bcf04275d1db8ed1b7257c763ebe6e7f122041d81f00d6aa954fbec531fa6c0b449d195a9aff199cc languageName: node linkType: hard -"@graphql-tools/schema@npm:^10.0.0, @graphql-tools/schema@npm:^10.0.23": - version: 10.0.23 - resolution: "@graphql-tools/schema@npm:10.0.23" +"@graphql-tools/schema@npm:^10.0.29, @graphql-tools/schema@npm:^10.0.31": + version: 10.0.31 + resolution: "@graphql-tools/schema@npm:10.0.31" dependencies: - "@graphql-tools/merge": "npm:^9.0.24" - "@graphql-tools/utils": "npm:^10.8.6" + "@graphql-tools/merge": "npm:^9.1.7" + "@graphql-tools/utils": "npm:^11.0.0" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/f0960dae161a478941276df1802af09844825c8135e4695b36f5f7e7384f43ff8e1288a67546023fc861951d783327f239112ccf563cb4be1f22038fc78acf21 + checksum: 10/5b775736f8b8454319e07cadc7d41bc5c9cc804a393490aaffd1bb7f59afddb02b498837e870bf98db4a1a989a721d5d8e2fd2b97409d078bac14503c2d4f9cb languageName: node linkType: hard @@ -9767,26 +9587,25 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/url-loader@npm:^8.0.0": - version: 8.0.0 - resolution: "@graphql-tools/url-loader@npm:8.0.0" +"@graphql-tools/url-loader@npm:^9.0.0": + version: 9.0.7 + resolution: "@graphql-tools/url-loader@npm:9.0.7" dependencies: - "@ardatan/sync-fetch": "npm:^0.0.1" - "@graphql-tools/delegate": "npm:^10.0.0" - "@graphql-tools/executor-graphql-ws": "npm:^1.0.0" - "@graphql-tools/executor-http": "npm:^1.0.0" - "@graphql-tools/executor-legacy-ws": "npm:^1.0.0" - "@graphql-tools/utils": "npm:^10.0.0" - "@graphql-tools/wrap": "npm:^10.0.0" + "@graphql-tools/executor-graphql-ws": "npm:^3.1.4" + "@graphql-tools/executor-http": "npm:^3.1.0" + "@graphql-tools/executor-legacy-ws": "npm:^1.1.25" + "@graphql-tools/utils": "npm:^11.0.0" + "@graphql-tools/wrap": "npm:^11.1.1" "@types/ws": "npm:^8.0.0" - "@whatwg-node/fetch": "npm:^0.9.0" + "@whatwg-node/fetch": "npm:^0.10.13" + "@whatwg-node/promise-helpers": "npm:^1.0.0" isomorphic-ws: "npm:^5.0.0" + sync-fetch: "npm:0.6.0" tslib: "npm:^2.4.0" - value-or-promise: "npm:^1.0.11" - ws: "npm:^8.12.0" + ws: "npm:^8.19.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/206065c2490e0747f6f9d756171b151017f9e5ad2d5f4c82c1644af8da3bf03e0075e4c55e6317e1823e74e32d307af5dd102f58851c7c361022578aa52ca8c1 + checksum: 10/292a43974e471a556697aa3ac170d9da7e6e3cc16e9462933f634aa8983cf5752805ba0c580bff1dbd7891168fe5e238b22c02782f88bee5923fa640e451e92e languageName: node linkType: hard @@ -9801,18 +9620,17 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.2, @graphql-tools/utils@npm:^10.0.5, @graphql-tools/utils@npm:^10.3.0, @graphql-tools/utils@npm:^10.8.6": - version: 10.8.6 - resolution: "@graphql-tools/utils@npm:10.8.6" +"@graphql-tools/utils@npm:^11.0.0": + version: 11.0.0 + resolution: "@graphql-tools/utils@npm:11.0.0" dependencies: "@graphql-typed-document-node/core": "npm:^3.1.1" "@whatwg-node/promise-helpers": "npm:^1.0.0" cross-inspect: "npm:1.0.1" - dset: "npm:^3.1.4" tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/98329aef966b489d3674eb086b784f6fb4500afaf9bc46fbe6a14ca32e98fec480c7395d3488c5eb2f450b75a538e98edf0527ed4bf24af352230e850c914389 + checksum: 10/4cc7577ab85d60908a1d5d448071b318791b798f571cd4b8e4289e0e0eeae9d7183b661a1a7d5da3cedaf5f9b62b936031e3a90d2e17a1c50acbd95d9106ba3c languageName: node linkType: hard @@ -9827,22 +9645,22 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/wrap@npm:^10.0.0": - version: 10.0.1 - resolution: "@graphql-tools/wrap@npm:10.0.1" +"@graphql-tools/wrap@npm:^11.1.1": + version: 11.1.8 + resolution: "@graphql-tools/wrap@npm:11.1.8" dependencies: - "@graphql-tools/delegate": "npm:^10.0.3" - "@graphql-tools/schema": "npm:^10.0.0" - "@graphql-tools/utils": "npm:^10.0.0" - tslib: "npm:^2.4.0" - value-or-promise: "npm:^1.0.12" + "@graphql-tools/delegate": "npm:^12.0.8" + "@graphql-tools/schema": "npm:^10.0.29" + "@graphql-tools/utils": "npm:^11.0.0" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + tslib: "npm:^2.8.1" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/0ccbb72a6d7bd67c10c86d4cc6da575b84ec2ba6488044dc7dfd6a046f09613d532f0af5549d481f2bfaf2f48c56cb087ea97021ebfe5e104531f80ec72e42a5 + checksum: 10/3ed15d0c92e5deed954a7e117b307af18ef8926132d356465938bbc66669da186aef1cd29cf72135d00ec86a3a149e216f83eaee83eb1fd2a0684a1ba1df473d languageName: node linkType: hard -"@graphql-typed-document-node/core@npm:3.2.0, @graphql-typed-document-node/core@npm:^3.1.1": +"@graphql-typed-document-node/core@npm:^3.1.1, @graphql-typed-document-node/core@npm:^3.2.0": version: 3.2.0 resolution: "@graphql-typed-document-node/core@npm:3.2.0" peerDependencies: @@ -9851,13 +9669,13 @@ __metadata: languageName: node linkType: hard -"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.11.1, @grpc/grpc-js@npm:^1.7.1": - version: 1.12.5 - resolution: "@grpc/grpc-js@npm:1.12.5" +"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.11.1, @grpc/grpc-js@npm:^1.14.3, @grpc/grpc-js@npm:^1.7.1": + version: 1.14.3 + resolution: "@grpc/grpc-js@npm:1.14.3" dependencies: - "@grpc/proto-loader": "npm:^0.7.13" + "@grpc/proto-loader": "npm:^0.8.0" "@js-sdsl/ordered-map": "npm:^4.4.2" - checksum: 10/4f8ead236dcab4d94e15e62d65ad2d93732d37f5cc52ffafe67ae00f69eae4a4c97d6d34a1b9eac9f30206468f2d15302ea6649afcba1d38929afa9d1e7c12d5 + checksum: 10/bb9bfe2f749179ae5ac7774d30486dfa2e0b004518c28de158b248e0f6f65f40138f01635c48266fa540670220f850216726e3724e1eb29d078817581c96e4db languageName: node linkType: hard @@ -9875,6 +9693,20 @@ __metadata: languageName: node linkType: hard +"@grpc/proto-loader@npm:^0.8.0": + version: 0.8.0 + resolution: "@grpc/proto-loader@npm:0.8.0" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.5.3" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10/216813bdca52cd3a84ac355ad93c2c3f54252be47327692fe666fd85baa5b1d50aa681ebc5626ab08926564fb2deae3b2ea435aa5bd883197650bbe56f2ae108 + languageName: node + linkType: hard + "@headlessui/react@npm:^1.7.15": version: 1.7.17 resolution: "@headlessui/react@npm:1.7.17" @@ -9888,11 +9720,11 @@ __metadata: linkType: hard "@hono/node-server@npm:^1.19.9": - version: 1.19.9 - resolution: "@hono/node-server@npm:1.19.9" + version: 1.19.10 + resolution: "@hono/node-server@npm:1.19.10" peerDependencies: hono: ^4 - checksum: 10/d4915c2e736ee1e3934b5538cde92b19914dc71346340528a04e4c7219afc7367965080cd1a5291ac9cbda7b0780b89b6ca93472a9418aa105d6d1183033dc8a + checksum: 10/3f4a386bf51d2eb0224522e7485052f8ca6485e9f59fb46cef8fe9578c0d2f35ae26900ed69b6c368250e79944239c70934912f85b833045b14ef19be557aa13 languageName: node linkType: hard @@ -10012,6 +9844,28 @@ __metadata: languageName: node linkType: hard +"@inquirer/core@npm:^6.0.0": + version: 6.0.0 + resolution: "@inquirer/core@npm:6.0.0" + dependencies: + "@inquirer/type": "npm:^1.1.6" + "@types/mute-stream": "npm:^0.0.4" + "@types/node": "npm:^20.10.7" + "@types/wrap-ansi": "npm:^3.0.0" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + cli-spinners: "npm:^2.9.2" + cli-width: "npm:^4.1.0" + figures: "npm:^3.2.0" + mute-stream: "npm:^1.0.0" + run-async: "npm:^3.0.0" + signal-exit: "npm:^4.1.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^6.2.0" + checksum: 10/a9f79fe538deab439afc845e16fa8832872b4562be6e39a5de8b50eca3e2b27be0e470fc4ee014f202a750213adc8a06068402d51d6d7b34b118b12b08200f85 + languageName: node + linkType: hard + "@inquirer/external-editor@npm:^1.0.0, @inquirer/external-editor@npm:^1.0.2": version: 1.0.3 resolution: "@inquirer/external-editor@npm:1.0.3" @@ -10034,6 +9888,28 @@ __metadata: languageName: node linkType: hard +"@inquirer/select@npm:1.3.3": + version: 1.3.3 + resolution: "@inquirer/select@npm:1.3.3" + dependencies: + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + figures: "npm:^3.2.0" + checksum: 10/0f33c51ab69c156b96092bfb107d08dd0f4227274917b9406aa23877e3ba94fd6738800fb973c44c051aaebdba72d07dc328df4b76e6e1285f68aa01a7ed0ed8 + languageName: node + linkType: hard + +"@inquirer/type@npm:^1.1.6": + version: 1.5.5 + resolution: "@inquirer/type@npm:1.5.5" + dependencies: + mute-stream: "npm:^1.0.0" + checksum: 10/bd3f3d7510785af4ad599e042e99e4be6380f52f79f3db140fe6fed0a605acf27b1a0a20fb5cc688eaf7b8aa0c36dacb1d89c7bba4586f38cbf58ba9f159e7b5 + languageName: node + linkType: hard + "@inquirer/type@npm:^3.0.0": version: 3.0.0 resolution: "@inquirer/type@npm:3.0.0" @@ -10043,6 +9919,15 @@ __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" @@ -10135,12 +10020,12 @@ __metadata: languageName: unknown linkType: soft -"@internationalized/date@npm:^3.10.1, @internationalized/date@npm:^3.11.0": - version: 3.11.0 - resolution: "@internationalized/date@npm:3.11.0" +"@internationalized/date@npm:^3.12.0": + version: 3.12.0 + resolution: "@internationalized/date@npm:3.12.0" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/52fcfc39b52ca9bf7893f034dd2704d0b7968e0c8d1038ff2de891bb4797d3326d020802c03307aa8ea1d94f1054f88fbc4db43179b4e74998d81a558f3acfcc + checksum: 10/0bc2e95385e156362a07a8788e93b4b380b43170cca75c19a9a72cb1840c1c701bb0d1fa67d9ba5e2fbc1e44b5bea29bca02b325d0daa2f960b92a71898ede92 languageName: node linkType: hard @@ -10179,22 +10064,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/balanced-match@npm:^4.0.1": - version: 4.0.1 - resolution: "@isaacs/balanced-match@npm:4.0.1" - checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 - languageName: node - linkType: hard - -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" - dependencies: - "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d - languageName: node - linkType: hard - "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -10209,6 +10078,13 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^9.0.0": + version: 9.0.0 + resolution: "@isaacs/cliui@npm:9.0.0" + checksum: 10/8ea3d1009fd29071419209bb91ede20cf27e6e2a1630c5e0702d8b3f47f9e1a3f1c5a587fa2cb96d22d18219790327df49db1bcced573346bbaf4577cf46b643 + languageName: node + linkType: hard + "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -10625,11 +10501,11 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:^0.6.3": - version: 0.6.3 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.3" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:^0.6.4": + version: 0.6.4 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.4" dependencies: - glob: "npm:^11.1.0" + glob: "npm:^13.0.1" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" @@ -10637,7 +10513,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/8a0f2c52b99e0dff4eeee574cf373b794e5398ee3c6b4d272378ccfe3b7dc2f3dc647d446716998ee2d88ee26df617e7b10da949702edcb625ee49e93b7daa95 + checksum: 10/9e1ead0120036e3acb2bfe4772c5c1f352ca43e19e3e59ef37aa7d08843302118a8d2313124c6058af60246058a6aa9c8895683e6d56d56b5c8cf20102078e04 languageName: node linkType: hard @@ -10746,6 +10622,15 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/base64@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/base64@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/ae44b0c4c83ecc5c0ee1911706a665e18e89d64a2b705cc458d7f6fc3c3c7db0e621261e978d02b74ded6a9fe1aafc8e708eb8a133e794a92bb033c50a0c4ccd + languageName: node + linkType: hard + "@jsonjoy.com/base64@npm:^1.1.2": version: 1.1.2 resolution: "@jsonjoy.com/base64@npm:1.1.2" @@ -10755,6 +10640,15 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/buffers@npm:17.67.0, @jsonjoy.com/buffers@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/buffers@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/6c8f6c4c73ec4ddab538a88be0bf72d8a934752755d43b0289fbe19ce9fa6123f082d1cd5ae179495e121a2f50267d26d36641f6dadedd8d5d2a2f980426e8ff + languageName: node + linkType: hard + "@jsonjoy.com/buffers@npm:^1.0.0, @jsonjoy.com/buffers@npm:^1.2.0": version: 1.2.1 resolution: "@jsonjoy.com/buffers@npm:1.2.1" @@ -10764,6 +10658,15 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/codegen@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/codegen@npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/e2462836c708999d045c4a15099f12e721089a3731f0ad33da210559a52ed763b8bddbec3c181857341984ef12ea355290609f37f0dc6f8de1545c028090adf5 + languageName: node + linkType: hard + "@jsonjoy.com/codegen@npm:^1.0.0": version: 1.0.0 resolution: "@jsonjoy.com/codegen@npm:1.0.0" @@ -10773,6 +10676,109 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/fs-core@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-core@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/46adecf53d79246eed43503e999f3068b38d6c5b209e305c5b6ca55cfbb68fd2780a2fe3ddc2de3535c51a6db76e4acaca67dc127d1ce54399cf786a5ccbd4ca + languageName: node + linkType: hard + +"@jsonjoy.com/fs-fsa@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-fsa@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-core": "npm:4.56.11" + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/ed322ec4596f0b385d17dfeca6f2e1c2de51aa7c356f092700ad7b8930816f27f7f49234ba5eb544f03d173ea3cf5f9509597a5c64f0c03d2c4c456b751e2a16 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-builtins@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-node-builtins@npm:4.56.11" + peerDependencies: + tslib: 2 + checksum: 10/04ca8c56cd3b883149b42cce5d9376d18445a071b6d88c5366f930c2ba93505a7d708ad8f5af1a15e691dd87ffb971048b906450287d5faf363472ced43ea884 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-to-fsa@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-node-to-fsa@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-fsa": "npm:4.56.11" + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + peerDependencies: + tslib: 2 + checksum: 10/e4e8b6ea18969665925b97055cb06636fbd74e5025cd126280ede2245ba2efe328146e0b7a5e45c45a64175dc74231caa982de4dda51bffb061c028c931f2fad + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node-utils@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-node-utils@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + peerDependencies: + tslib: 2 + checksum: 10/72ca92de077c34d26c935b1ec6011de587bc2f382e822d9e57c76318d506f3c40f5075d04ede666b59d45e275e577ea77032a10b48161483040f3e3f7c381232 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-node@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-node@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-core": "npm:4.56.11" + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + "@jsonjoy.com/fs-print": "npm:4.56.11" + "@jsonjoy.com/fs-snapshot": "npm:4.56.11" + glob-to-regex.js: "npm:^1.0.0" + thingies: "npm:^2.5.0" + peerDependencies: + tslib: 2 + checksum: 10/a6a7e84de467d644bd2b23f34afaf61e93eb05245f40325a067403d2af5b4b22adce806639b80ff0f8431049dda706a803e6f73f2bf944d73a1ad78ac04bbd15 + languageName: node + linkType: hard + +"@jsonjoy.com/fs-print@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-print@npm:4.56.11" + dependencies: + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + tree-dump: "npm:^1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/352fb4932c0ed44b5fb148b253754748b6d3e9859452ef1b03cebb6b9601e9f84336ef0932ca4de9687b2f3ec703083d4572c7bf2036537f787673229c3ed68b + languageName: node + linkType: hard + +"@jsonjoy.com/fs-snapshot@npm:4.56.11": + version: 4.56.11 + resolution: "@jsonjoy.com/fs-snapshot@npm:4.56.11" + dependencies: + "@jsonjoy.com/buffers": "npm:^17.65.0" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + "@jsonjoy.com/json-pack": "npm:^17.65.0" + "@jsonjoy.com/util": "npm:^17.65.0" + peerDependencies: + tslib: 2 + checksum: 10/97665962c5bf609720ac2913111da540235fa8aed52241e017da16ae2ac0228ae9e1938906d51e61f85fdaa0511c46db22b3c8212c9ba77a0ae4082cd20e4d57 + languageName: node + linkType: hard + "@jsonjoy.com/json-pack@npm:^1.11.0": version: 1.21.0 resolution: "@jsonjoy.com/json-pack@npm:1.21.0" @@ -10791,6 +10797,35 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/json-pack@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/json-pack@npm:17.67.0" + dependencies: + "@jsonjoy.com/base64": "npm:17.67.0" + "@jsonjoy.com/buffers": "npm:17.67.0" + "@jsonjoy.com/codegen": "npm:17.67.0" + "@jsonjoy.com/json-pointer": "npm:17.67.0" + "@jsonjoy.com/util": "npm:17.67.0" + hyperdyperid: "npm:^1.2.0" + thingies: "npm:^2.5.0" + tree-dump: "npm:^1.1.0" + peerDependencies: + tslib: 2 + checksum: 10/9ff4403862e49433fe607175e90af749d64902640d63919ba559e5748d1a3db60d7366cc3b84dcc4a57ad478540e5eecb22fed80766e293482a0ab8e583b1b0b + languageName: node + linkType: hard + +"@jsonjoy.com/json-pointer@npm:17.67.0": + version: 17.67.0 + resolution: "@jsonjoy.com/json-pointer@npm:17.67.0" + dependencies: + "@jsonjoy.com/util": "npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/5a27c6b5b1276d357cfc3e8a05112d6305ccd17bf672190f25dfac2f4108ced170e784451d64728f60f93305c0007e3f832ddd175b8a47f3eb652cbabcec31ad + languageName: node + linkType: hard + "@jsonjoy.com/json-pointer@npm:^1.0.2": version: 1.0.2 resolution: "@jsonjoy.com/json-pointer@npm:1.0.2" @@ -10803,6 +10838,18 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/util@npm:17.67.0, @jsonjoy.com/util@npm:^17.65.0": + version: 17.67.0 + resolution: "@jsonjoy.com/util@npm:17.67.0" + dependencies: + "@jsonjoy.com/buffers": "npm:17.67.0" + "@jsonjoy.com/codegen": "npm:17.67.0" + peerDependencies: + tslib: 2 + checksum: 10/b0facf65c3190d6ed1ada7e5b7679d80fa5da73bfbd02f2bb2f3af1c28c0d854b6ee2350824313b7ba82c0e5191da94903b4af61255bc232dbb7feedd2f31e0c + languageName: node + linkType: hard + "@jsonjoy.com/util@npm:^1.9.0": version: 1.9.0 resolution: "@jsonjoy.com/util@npm:1.9.0" @@ -10928,6 +10975,15 @@ __metadata: languageName: node linkType: hard +"@kwsites/file-exists@npm:^1.1.1": + version: 1.1.1 + resolution: "@kwsites/file-exists@npm:1.1.1" + dependencies: + debug: "npm:^4.1.1" + checksum: 10/4ff945de7293285133aeae759caddc71e73c4a44a12fac710fdd4f574cce2671a3f89d8165fdb03d383cfc97f3f96f677d8de3c95133da3d0e12a123a23109fe + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.3 resolution: "@leichtgewicht/ip-codec@npm:2.0.3" @@ -11255,27 +11311,38 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor@npm:^7.55.1": - version: 7.55.2 - resolution: "@microsoft/api-extractor@npm:7.55.2" +"@microsoft/api-extractor-model@npm:7.33.1": + version: 7.33.1 + resolution: "@microsoft/api-extractor-model@npm:7.33.1" dependencies: - "@microsoft/api-extractor-model": "npm:7.32.2" "@microsoft/tsdoc": "npm:~0.16.0" "@microsoft/tsdoc-config": "npm:~0.18.0" - "@rushstack/node-core-library": "npm:5.19.1" - "@rushstack/rig-package": "npm:0.6.0" - "@rushstack/terminal": "npm:0.19.5" - "@rushstack/ts-command-line": "npm:5.1.5" + "@rushstack/node-core-library": "npm:5.20.1" + checksum: 10/cb267ca0020a68b84570bc99e974d050acf8b17a47f1999998a9dbc2ef81453f8188a93970a6b2274890a5dd5015502b6cebe94da06d3583e65ca490dabf4c1e + languageName: node + linkType: hard + +"@microsoft/api-extractor@npm:^7.57.3": + version: 7.57.3 + resolution: "@microsoft/api-extractor@npm:7.57.3" + dependencies: + "@microsoft/api-extractor-model": "npm:7.33.1" + "@microsoft/tsdoc": "npm:~0.16.0" + "@microsoft/tsdoc-config": "npm:~0.18.0" + "@rushstack/node-core-library": "npm:5.20.1" + "@rushstack/rig-package": "npm:0.7.1" + "@rushstack/terminal": "npm:0.22.1" + "@rushstack/ts-command-line": "npm:5.3.1" diff: "npm:~8.0.2" - lodash: "npm:~4.17.15" - minimatch: "npm:10.0.3" + lodash: "npm:~4.17.23" + minimatch: "npm:10.2.1" resolve: "npm:~1.22.1" semver: "npm:~7.5.4" source-map: "npm:~0.6.1" typescript: "npm:5.8.2" bin: api-extractor: bin/api-extractor - checksum: 10/56b7e9338ad18cf3dc6aaefd679b90117c9d5498dee5c621e868a5fe5002656e62d08267525eb880221d4588afcf6d680604249a9c2fb5faaba8f9c87be16b3e + checksum: 10/5b2a8c1833b97db4df49aa0d326b4591474a241da2c71489b5de9f24cc42f7579a2ff45d44bc52302c8f4ffb9dd0fbd6e101f19231003dafd31c0efc8100e2ba languageName: node linkType: hard @@ -11313,8 +11380,8 @@ __metadata: linkType: hard "@modelcontextprotocol/sdk@npm:^1.25.2": - version: 1.26.0 - resolution: "@modelcontextprotocol/sdk@npm:1.26.0" + version: 1.27.1 + resolution: "@modelcontextprotocol/sdk@npm:1.27.1" dependencies: "@hono/node-server": "npm:^1.19.9" ajv: "npm:^8.17.1" @@ -11341,7 +11408,7 @@ __metadata: optional: true zod: optional: false - checksum: 10/a206b2a4d61a23be8b8f4c886528dd9348d11b17ce36013b350edf5c082b1c1f07941d52ea098f721daf3828085b6f6276bb844c484a0e9913edbc028517a3d5 + checksum: 10/3cb0d61cfb916e555c85b4a527e772f88fcf9c6abacbe5eb5e965aac7c898190c416341ab3b3cba8c2d5f5ce4d513279fba3ad7784a0903d7ccd335decc55395 languageName: node linkType: hard @@ -11955,9 +12022,9 @@ __metadata: languageName: node linkType: hard -"@nestjs/common@npm:11.1.12": - version: 11.1.12 - resolution: "@nestjs/common@npm:11.1.12" +"@nestjs/common@npm:11.1.16": + version: 11.1.16 + resolution: "@nestjs/common@npm:11.1.16" dependencies: file-type: "npm:21.3.0" iterare: "npm:1.2.1" @@ -11974,13 +12041,13 @@ __metadata: optional: true class-validator: optional: true - checksum: 10/0e8a0b73453f1e77ae1be6eabc68b8374e6a8a39cf46d5beafaa398fa72aed6b653df91b72e5b2aa2225251b1463ca79d8424d207a981f586ac216441f83ac59 + checksum: 10/23b0bd734333f17c97712d441d5ba0ff1981968f4fe1d3b3b8b0eafd3598c97f16b40e29d14f9cbf0dd3dfd98d665fe532f85b91dabc36c82f0783cb31160b30 languageName: node linkType: hard -"@nestjs/core@npm:11.1.12": - version: 11.1.12 - resolution: "@nestjs/core@npm:11.1.12" +"@nestjs/core@npm:11.1.16": + version: 11.1.16 + resolution: "@nestjs/core@npm:11.1.16" dependencies: "@nuxt/opencollective": "npm:0.4.1" fast-safe-stringify: "npm:2.1.1" @@ -12002,7 +12069,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: 10/44f122101f411abb2b83cd6bab865006c4a74552d3903ca47a58b396608453b36b59cd6461f8689cd31c3d91b81960cea190d654448921ae0efedbd20ff52284 + checksum: 10/a4e53b3877a503c78c763b2535307a5c43133e114ee826640b1e4d8feb5142a36e16f5db591e8d2077626a28e4e5cf2076fbacc217e0141557e21a3b85649326 languageName: node linkType: hard @@ -13010,29 +13077,29 @@ __metadata: linkType: hard "@openapitools/openapi-generator-cli@npm:^2.4.26, @openapitools/openapi-generator-cli@npm:^2.7.0": - version: 2.28.0 - resolution: "@openapitools/openapi-generator-cli@npm:2.28.0" + version: 2.30.2 + resolution: "@openapitools/openapi-generator-cli@npm:2.30.2" dependencies: + "@inquirer/select": "npm:1.3.3" "@nestjs/axios": "npm:4.0.1" - "@nestjs/common": "npm:11.1.12" - "@nestjs/core": "npm:11.1.12" + "@nestjs/common": "npm:11.1.16" + "@nestjs/core": "npm:11.1.16" "@nuxtjs/opencollective": "npm:0.3.2" - axios: "npm:1.13.2" + axios: "npm:^1.13.6" chalk: "npm:4.1.2" commander: "npm:8.3.0" compare-versions: "npm:6.1.1" concurrently: "npm:9.2.1" console.table: "npm:0.10.0" - fs-extra: "npm:11.3.3" - glob: "npm:13.0.0" - inquirer: "npm:8.2.7" + fs-extra: "npm:11.3.4" + glob: "npm:13.0.6" proxy-agent: "npm:6.5.0" reflect-metadata: "npm:0.2.2" rxjs: "npm:7.8.2" tslib: "npm:2.8.1" bin: openapi-generator-cli: main.js - checksum: 10/42f6c887d8a16eca90420139e1d0d464bd78aad1d609a8a9589c3fbcda4ab3052f6b9310b7b54521583df50f2dfb4cb0c9de073e717e91eadbf4e230e70f3007 + checksum: 10/247d3e3fce575440a6ce45c28b5851b4f07c6dd414a1f71526109e69f3768f1379871f92f822075625fd95c038cd5e8c11f486060a3f1c5a82ac3e5ec620acc7 languageName: node linkType: hard @@ -13050,15 +13117,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.208.0, @opentelemetry/api-logs@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/api-logs@npm:0.208.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10/ae339416a244e90b1718af1ed5430348188be60871f3799c847bab409bba1513337cac7da40b4883bf7f280680754319b44a9dc95fa2879d15c0413c9955b145 - languageName: node - linkType: hard - "@opentelemetry/api-logs@npm:0.211.0": version: 0.211.0 resolution: "@opentelemetry/api-logs@npm:0.211.0" @@ -13068,6 +13126,15 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api-logs@npm:0.213.0, @opentelemetry/api-logs@npm:^0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/api-logs@npm:0.213.0" + dependencies: + "@opentelemetry/api": "npm:^1.3.0" + checksum: 10/9b2d030d8534520f23e3903ccf64dc479fb4d37fcf5ca265ca7de439ec8dd5c849aaa62068278cd97879da7c9528e34c7162cc7bbd025dd8d74667843adc151f + languageName: node + linkType: hard + "@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.9.0, @opentelemetry/api@npm:~1.9.0": version: 1.9.0 resolution: "@opentelemetry/api@npm:1.9.0" @@ -13075,63 +13142,63 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/auto-instrumentations-node@npm:^0.67.0": - version: 0.67.3 - resolution: "@opentelemetry/auto-instrumentations-node@npm:0.67.3" +"@opentelemetry/auto-instrumentations-node@npm:^0.71.0": + version: 0.71.0 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.71.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" - "@opentelemetry/instrumentation-amqplib": "npm:^0.56.0" - "@opentelemetry/instrumentation-aws-lambda": "npm:^0.61.1" - "@opentelemetry/instrumentation-aws-sdk": "npm:^0.64.1" - "@opentelemetry/instrumentation-bunyan": "npm:^0.54.0" - "@opentelemetry/instrumentation-cassandra-driver": "npm:^0.54.1" - "@opentelemetry/instrumentation-connect": "npm:^0.52.0" - "@opentelemetry/instrumentation-cucumber": "npm:^0.24.0" - "@opentelemetry/instrumentation-dataloader": "npm:^0.26.1" - "@opentelemetry/instrumentation-dns": "npm:^0.52.0" - "@opentelemetry/instrumentation-express": "npm:^0.57.1" - "@opentelemetry/instrumentation-fastify": "npm:^0.53.1" - "@opentelemetry/instrumentation-fs": "npm:^0.28.0" - "@opentelemetry/instrumentation-generic-pool": "npm:^0.52.0" - "@opentelemetry/instrumentation-graphql": "npm:^0.56.0" - "@opentelemetry/instrumentation-grpc": "npm:^0.208.0" - "@opentelemetry/instrumentation-hapi": "npm:^0.55.1" - "@opentelemetry/instrumentation-http": "npm:^0.208.0" - "@opentelemetry/instrumentation-ioredis": "npm:^0.57.0" - "@opentelemetry/instrumentation-kafkajs": "npm:^0.18.1" - "@opentelemetry/instrumentation-knex": "npm:^0.53.1" - "@opentelemetry/instrumentation-koa": "npm:^0.57.1" - "@opentelemetry/instrumentation-lru-memoizer": "npm:^0.53.1" - "@opentelemetry/instrumentation-memcached": "npm:^0.52.1" - "@opentelemetry/instrumentation-mongodb": "npm:^0.62.0" - "@opentelemetry/instrumentation-mongoose": "npm:^0.55.1" - "@opentelemetry/instrumentation-mysql": "npm:^0.55.0" - "@opentelemetry/instrumentation-mysql2": "npm:^0.55.1" - "@opentelemetry/instrumentation-nestjs-core": "npm:^0.55.0" - "@opentelemetry/instrumentation-net": "npm:^0.53.0" - "@opentelemetry/instrumentation-openai": "npm:^0.7.1" - "@opentelemetry/instrumentation-oracledb": "npm:^0.34.1" - "@opentelemetry/instrumentation-pg": "npm:^0.61.2" - "@opentelemetry/instrumentation-pino": "npm:^0.55.1" - "@opentelemetry/instrumentation-redis": "npm:^0.57.2" - "@opentelemetry/instrumentation-restify": "npm:^0.54.0" - "@opentelemetry/instrumentation-router": "npm:^0.53.0" - "@opentelemetry/instrumentation-runtime-node": "npm:^0.22.0" - "@opentelemetry/instrumentation-socket.io": "npm:^0.55.1" - "@opentelemetry/instrumentation-tedious": "npm:^0.28.0" - "@opentelemetry/instrumentation-undici": "npm:^0.19.0" - "@opentelemetry/instrumentation-winston": "npm:^0.53.0" - "@opentelemetry/resource-detector-alibaba-cloud": "npm:^0.32.0" - "@opentelemetry/resource-detector-aws": "npm:^2.9.0" - "@opentelemetry/resource-detector-azure": "npm:^0.17.0" - "@opentelemetry/resource-detector-container": "npm:^0.8.0" - "@opentelemetry/resource-detector-gcp": "npm:^0.44.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/instrumentation-amqplib": "npm:^0.60.0" + "@opentelemetry/instrumentation-aws-lambda": "npm:^0.65.0" + "@opentelemetry/instrumentation-aws-sdk": "npm:^0.68.0" + "@opentelemetry/instrumentation-bunyan": "npm:^0.58.0" + "@opentelemetry/instrumentation-cassandra-driver": "npm:^0.58.0" + "@opentelemetry/instrumentation-connect": "npm:^0.56.0" + "@opentelemetry/instrumentation-cucumber": "npm:^0.29.0" + "@opentelemetry/instrumentation-dataloader": "npm:^0.30.0" + "@opentelemetry/instrumentation-dns": "npm:^0.56.0" + "@opentelemetry/instrumentation-express": "npm:^0.61.0" + "@opentelemetry/instrumentation-fastify": "npm:^0.57.0" + "@opentelemetry/instrumentation-fs": "npm:^0.32.0" + "@opentelemetry/instrumentation-generic-pool": "npm:^0.56.0" + "@opentelemetry/instrumentation-graphql": "npm:^0.61.0" + "@opentelemetry/instrumentation-grpc": "npm:^0.213.0" + "@opentelemetry/instrumentation-hapi": "npm:^0.59.0" + "@opentelemetry/instrumentation-http": "npm:^0.213.0" + "@opentelemetry/instrumentation-ioredis": "npm:^0.61.0" + "@opentelemetry/instrumentation-kafkajs": "npm:^0.22.0" + "@opentelemetry/instrumentation-knex": "npm:^0.57.0" + "@opentelemetry/instrumentation-koa": "npm:^0.61.0" + "@opentelemetry/instrumentation-lru-memoizer": "npm:^0.57.0" + "@opentelemetry/instrumentation-memcached": "npm:^0.56.0" + "@opentelemetry/instrumentation-mongodb": "npm:^0.66.0" + "@opentelemetry/instrumentation-mongoose": "npm:^0.59.0" + "@opentelemetry/instrumentation-mysql": "npm:^0.59.0" + "@opentelemetry/instrumentation-mysql2": "npm:^0.59.0" + "@opentelemetry/instrumentation-nestjs-core": "npm:^0.59.0" + "@opentelemetry/instrumentation-net": "npm:^0.57.0" + "@opentelemetry/instrumentation-openai": "npm:^0.11.0" + "@opentelemetry/instrumentation-oracledb": "npm:^0.38.0" + "@opentelemetry/instrumentation-pg": "npm:^0.65.0" + "@opentelemetry/instrumentation-pino": "npm:^0.59.0" + "@opentelemetry/instrumentation-redis": "npm:^0.61.0" + "@opentelemetry/instrumentation-restify": "npm:^0.58.0" + "@opentelemetry/instrumentation-router": "npm:^0.57.0" + "@opentelemetry/instrumentation-runtime-node": "npm:^0.26.0" + "@opentelemetry/instrumentation-socket.io": "npm:^0.60.0" + "@opentelemetry/instrumentation-tedious": "npm:^0.32.0" + "@opentelemetry/instrumentation-undici": "npm:^0.23.0" + "@opentelemetry/instrumentation-winston": "npm:^0.57.0" + "@opentelemetry/resource-detector-alibaba-cloud": "npm:^0.33.3" + "@opentelemetry/resource-detector-aws": "npm:^2.13.0" + "@opentelemetry/resource-detector-azure": "npm:^0.21.0" + "@opentelemetry/resource-detector-container": "npm:^0.8.4" + "@opentelemetry/resource-detector-gcp": "npm:^0.48.0" "@opentelemetry/resources": "npm:^2.0.0" - "@opentelemetry/sdk-node": "npm:^0.208.0" + "@opentelemetry/sdk-node": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.4.1 "@opentelemetry/core": ^2.0.0 - checksum: 10/e14b9b8182ada889d4f0ddfdce46f0ce2d07295807549e6785da6f0292e75b2e36f9b248e01b45fd4057be8f8c813f9325d4936a8e0bb8bded0bf934dd2fd370 + checksum: 10/a31c3a9118b9cdf8e9a44f9a26c2c4d3bebc9f3c23f3f7bb37a6de0d9ab93bf3f1f46ddc4862998ac9cf11c6a4d169a05dfb1a5b02984335bfba5b6348122c3d languageName: node linkType: hard @@ -13147,12 +13214,15 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/context-async-hooks@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/context-async-hooks@npm:2.2.0" +"@opentelemetry/configuration@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/configuration@npm:0.213.0" + dependencies: + "@opentelemetry/core": "npm:2.6.0" + yaml: "npm:^2.0.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/00ee1a35ce9f96632955830d54672025db6d4dc77d440c183cd9751efd5bdfeb8a078094d3d1caaf8b3c250def098990a98bfd8254b1ec04b759f1e3f2fc224e + "@opentelemetry/api": ^1.9.0 + checksum: 10/2bf7ef3a73849d6c618cf04f469009cf46c004df43c014e2f2a60b17692b85eec261fb362f1d4d7276fd128681d35ac07493cb4ef743505fec5d6cbff1a446e2 languageName: node linkType: hard @@ -13165,18 +13235,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/core@npm:2.2.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:^1.29.0" +"@opentelemetry/context-async-hooks@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/context-async-hooks@npm:2.6.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/f25193ba8b1fadb7bd8ed0d86ac39dd0f3fd3eec47c2fb2745bd22442b2d5e3ca88e5cab6d97111349d3182bf8e4356f8b7c7213ebea8f7719de944ce13a19cb + checksum: 10/a1f746fb9bb25b4c40c0da4cc68a7412e82f120a6ddc80dcf0117432418e64c947527bca87895ebce4211a09992863a75a0f5b5f8e7185a573244cccb4809c42 languageName: node linkType: hard -"@opentelemetry/core@npm:2.5.0, @opentelemetry/core@npm:^2.0.0": +"@opentelemetry/core@npm:2.5.0": version: 2.5.0 resolution: "@opentelemetry/core@npm:2.5.0" dependencies: @@ -13187,6 +13255,17 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/core@npm:2.6.0, @opentelemetry/core@npm:^2.0.0": + version: 2.6.0 + resolution: "@opentelemetry/core@npm:2.6.0" + dependencies: + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10/21c017cc68fe7836d06ecac31abeba6ad610dd42e9c1cb9562cd13eed3f644c48111a1fc7d00dfc2b7cc179d40f059482c69c978c24352ac0c605f30686a01a4 + languageName: node + linkType: hard + "@opentelemetry/core@npm:^1.29.0": version: 1.30.1 resolution: "@opentelemetry/core@npm:1.30.1" @@ -13198,22 +13277,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-logs-otlp-grpc@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.208.0" - dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-grpc-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/ed1d00184b8d03aed376a39363c1561eb4d26155147b53dda36f9c9c29e1e144a783c8d5dbeb1f91a157243777d7261b5e24c38202c5cec6d85dc6c45643d96c - languageName: node - linkType: hard - "@opentelemetry/exporter-logs-otlp-grpc@npm:0.211.0": version: 0.211.0 resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.211.0" @@ -13230,18 +13293,19 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-logs-otlp-http@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-logs-otlp-http@npm:0.208.0" +"@opentelemetry/exporter-logs-otlp-grpc@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.213.0" dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" + "@grpc/grpc-js": "npm:^1.14.3" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/sdk-logs": "npm:0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/b200e95bc71bc3c6591ef1f38a37ae27de5d2164a0e0df2b319369f026aaa6df080ed5d5fab02906b35f4c6e02a829ba0b09e041b506ade4b5c3240caafc420d + checksum: 10/5c852a8dbf9f1fc00b8af22689a1971af8db6a4d1c3ddcce516877a07b5b40c28dbb6c71549a73c8e8953db3602a8671666f22d1f0aca40fa1f0c1fbbbfbff33 languageName: node linkType: hard @@ -13260,20 +13324,18 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-logs-otlp-proto@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-logs-otlp-proto@npm:0.208.0" +"@opentelemetry/exporter-logs-otlp-http@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-logs-otlp-http@npm:0.213.0" dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" + "@opentelemetry/api-logs": "npm:0.213.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/sdk-logs": "npm:0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/71e11a3cb0fdca976ce644443846e4aaaad915848c99a212369e6b2b0cba664bf0cfbccc270f1d4210c6fc925b08a2dfb58f263d219fac39c5243993dcba7aa1 + checksum: 10/f9ee8f8d3d7113b96416c658ba17c9183e23365ba1ee1f3e3124a435f0d989367b6569cce201defcc295650af22ffbe766614704f19c5361caf4a058817184b6 languageName: node linkType: hard @@ -13294,21 +13356,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-metrics-otlp-grpc@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-metrics-otlp-grpc@npm:0.208.0" +"@opentelemetry/exporter-logs-otlp-proto@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-logs-otlp-proto@npm:0.213.0" dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/exporter-metrics-otlp-http": "npm:0.208.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-grpc-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" + "@opentelemetry/api-logs": "npm:0.213.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-logs": "npm:0.213.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/495ac28c27781750ae19d1e6405e5dd9ff277547e4ab1015e4b3fd2a1d8c26b0977a62dd20ac4e263a980bbace6a2fbe8fed91ae289dddfed6a5a55db0df861e + checksum: 10/4229f62eb802da49e75820188f77b12c3fdde4f0522e1df01cc097307baba8b141587d78695edd71343f40e811e946909f5c648436edfeef6b03e367e340b7e6 languageName: node linkType: hard @@ -13330,18 +13391,21 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-metrics-otlp-http@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.208.0" +"@opentelemetry/exporter-metrics-otlp-grpc@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-metrics-otlp-grpc@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" + "@grpc/grpc-js": "npm:^1.14.3" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.213.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/ea4e14481e3a7cd08574f811bf62527305a735972818cc4bb4b2232d19c65732200266de16ae0a59dafd8aa27b7acff77af6e3e300df457841b7e2f5f97ee43b + checksum: 10/b1914ec0a2017312b69320214f09c3c2319baf4da5da3377c2d9baf6ec85ebaf7c4cb6ea1d129224bf40f8fb8225072b8fbfa089524ab7383eda0251d4f4c724 languageName: node linkType: hard @@ -13360,19 +13424,18 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-metrics-otlp-proto@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.208.0" +"@opentelemetry/exporter-metrics-otlp-http@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/exporter-metrics-otlp-http": "npm:0.208.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/06d96d582294e3f792508f49a2138b926bde85545057260a97bb7aaed7b935f424e41c25c9b94f5781a1902bc77bcf9c0308d4a8d9e9e1539eb89567adb8b28f + checksum: 10/1cbb059b2b3d7b1a183c5ce1b4aba1a7a7e43dd6983af30191be19f406930914fc666dc20e0f8db4422cf2a83b0c6416612663e74c5cfedd8e2487fbd20257ea languageName: node linkType: hard @@ -13392,16 +13455,19 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-prometheus@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-prometheus@npm:0.208.0" +"@opentelemetry/exporter-metrics-otlp-proto@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.213.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/3aae6be7f694e8e7add17c76f7c37660003026fc1feb3f5e2d4b6ff171426a96fd595384b7ecca19a5b2a58d887a3fc87906d4524dd8fb8424a1022d595dabb6 + checksum: 10/c1d9d94cf2ccf4daf495046f2d00675985fcfe02f1ed5db538c9ea040123cae767c7ae29f470dca138bcbe7dbea2c92f9accd8e4e27f540792c6f411169c7bb1 languageName: node linkType: hard @@ -13418,20 +13484,17 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-grpc@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.208.0" +"@opentelemetry/exporter-prometheus@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-prometheus@npm:0.213.0" dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-grpc-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5405fb3ecf2ab9fcb8279e13ed89eb490ea645cfd9662a064d9bbd7e97c8ff838cd52908d14e27509aa9d00dbded22a9bc707436c54220c114d29c51abddb31f + checksum: 10/037631feebd40b1866f1eafd0e16fa09b11da5ab869a744768aad4defba26db1c03e1d1e84c806c478123719ec2e7390b41e31e3049a3a4ad8d5dafbc51e34af languageName: node linkType: hard @@ -13452,18 +13515,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.208.0" +"@opentelemetry/exporter-trace-otlp-grpc@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" + "@grpc/grpc-js": "npm:^1.14.3" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/69cbd8a223ddd774db97fe3b8e55063ac02e78502a7378596c14101dc98256cef3a5ef0092808ab73d42d09393e000a9eb2041c0d3c087efeb3864bb57f49907 + checksum: 10/f3b7e98f28427f2459c635581f659bc9b1494620001a23d2ef5ee1b48e4f6730a5fae7d958692aa24445ea084b9938e9bc6ea2ca24bde6ae75419005e5d4d49a languageName: node linkType: hard @@ -13482,18 +13547,18 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-proto@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.208.0" +"@opentelemetry/exporter-trace-otlp-http@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/af948730fd917471c220fcb9ad5c05bb3ab365e1cd8429fb69f50e855aa76d516a64307c52bb7e756fdd768c02806940d5fe060506191d231903e7db158dfb12 + checksum: 10/e8f6ad2eee843625e9e1efd9e47b1325dde1e3e9e7a4e2cfe4f5a6bece4e2e422431270b4530a87383c36d365f65a2ebee6ca5bab797d86ec585407dd3cd675f languageName: node linkType: hard @@ -13512,17 +13577,18 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-zipkin@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/exporter-zipkin@npm:2.2.0" +"@opentelemetry/exporter-trace-otlp-proto@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10/5e7b1464810ab2de77cee437dfc4436ade5c5391a9b80cdd5c1495f3aa32cf8e4cf458cae327dc477260876a8a9ef01bdb5bb3a41d37aaaaf392f2a00185d238 + "@opentelemetry/api": ^1.3.0 + checksum: 10/839d7670e1598a25e5a76bf066cdd0951b5eff794f3f1393b1eb32a7d4b9e3b7a53cdadfd5279a0631dacff7a2a9c034de21562a65a4520b7493e5f07409891f languageName: node linkType: hard @@ -13540,526 +13606,530 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-amqplib@npm:^0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/instrumentation-amqplib@npm:0.56.0" +"@opentelemetry/exporter-zipkin@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/exporter-zipkin@npm:2.6.0" dependencies: - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" - "@opentelemetry/semantic-conventions": "npm:^1.33.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/433377a73ba83f9b1008ab9868a686109244bb51ea87b6ffa54939b23fc4ac63ac1dafdb2d9b9720382d194ba638135cd0762277ae0a2e86f6a5b02aa4513ca8 + "@opentelemetry/api": ^1.0.0 + checksum: 10/dcae93429cf76880e422634a9da41b6c026d968e1a18413d5318bf55540539dc3ec949976b391f8254e92567dbfa144bc3db208dfd79489c318d24d10062c234 languageName: node linkType: hard -"@opentelemetry/instrumentation-aws-lambda@npm:^0.61.1": - version: 0.61.1 - resolution: "@opentelemetry/instrumentation-aws-lambda@npm:0.61.1" +"@opentelemetry/instrumentation-amqplib@npm:^0.60.0": + version: 0.60.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.60.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/fcf55c6489dd59575bd33de44ff2c6dd1ce2af06d71188df7d81791bd1c6b54414c63518a8c645f56cf08e535cfca211afa023df48b8ce168d3069190e769666 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-aws-lambda@npm:^0.65.0": + version: 0.65.0 + resolution: "@opentelemetry/instrumentation-aws-lambda@npm:0.65.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" "@types/aws-lambda": "npm:^8.10.155" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/04167ed3ec275c045f0fc1f82dfdb09d4f54744bb87929e26ac4f51de8fb57139c57cb0570ad8e81f382a45f8c8ca1c241636249651a0a7961cbe89368856a23 + checksum: 10/08eff3816df64f6dfb189070abf10163f98f837751e2311720d422a0f5b56fc26100eacccc607a639912ac2e58d0e4e9babb486b33629c6971cede4190c73d0e languageName: node linkType: hard -"@opentelemetry/instrumentation-aws-sdk@npm:^0.64.1": - version: 0.64.1 - resolution: "@opentelemetry/instrumentation-aws-sdk@npm:0.64.1" +"@opentelemetry/instrumentation-aws-sdk@npm:^0.68.0": + version: 0.68.0 + resolution: "@opentelemetry/instrumentation-aws-sdk@npm:0.68.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.34.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/37cbdace1541980e95c2fb4f43859c7ccca1f85dce009e69bf2e957278190fa627be93fe3a2bcd112709c1028213d5ef897450c1f247181e0ab4d4e372c368df + checksum: 10/bb5851b446067c05aef200cc28ca9b4ca941ee7308ab82039c915aec8fc97bafd5ea8ed1b6f01053a1e7b31caa21ad8a013779f7db77018115b2be630ddfb623 languageName: node linkType: hard -"@opentelemetry/instrumentation-bunyan@npm:^0.54.0": - version: 0.54.0 - resolution: "@opentelemetry/instrumentation-bunyan@npm:0.54.0" +"@opentelemetry/instrumentation-bunyan@npm:^0.58.0": + version: 0.58.0 + resolution: "@opentelemetry/instrumentation-bunyan@npm:0.58.0" dependencies: - "@opentelemetry/api-logs": "npm:^0.208.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/api-logs": "npm:^0.213.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@types/bunyan": "npm:1.8.11" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/563eeb0788c551e03947730586ee75a32f9fac28512fb97f109eccd582fe3c049d8642c9f1a638ca028215899d83c8cc6f0b3f0c36467c5e69e7160dd81c0bc4 + checksum: 10/5ba6b704793c02bc51c3fa4507c7ac943e4adf69e0e62f6d63bab13c3cbadb724b79b8d9d385518efd6e8165ad723e7d2d14631d3d6a1630ec92b8da4d82fe5c languageName: node linkType: hard -"@opentelemetry/instrumentation-cassandra-driver@npm:^0.54.1": - version: 0.54.1 - resolution: "@opentelemetry/instrumentation-cassandra-driver@npm:0.54.1" +"@opentelemetry/instrumentation-cassandra-driver@npm:^0.58.0": + version: 0.58.0 + resolution: "@opentelemetry/instrumentation-cassandra-driver@npm:0.58.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.37.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/93089f2d9d657ce69f3ae6f0b86ce467b899744defd94b98351a1081ae470ef01f82496ca623edf089aca7391d8911f11fd94499306ea935c61e2e342638d440 + checksum: 10/c6bdc86e7c24737a0d08c031b88f5e863482d3da126bce7711f5b92fd840f49f31834a38a6f530100042f5d0cea6da549b813489665331d01a15ce4e034242e3 languageName: node linkType: hard -"@opentelemetry/instrumentation-connect@npm:^0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-connect@npm:0.52.0" +"@opentelemetry/instrumentation-connect@npm:^0.56.0": + version: 0.56.0 + resolution: "@opentelemetry/instrumentation-connect@npm:0.56.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" "@types/connect": "npm:3.4.38" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/c526768a10a4f6c76b791e382edeae2217a7b3c448b1a3458ef34ca46a645b6cf67eeec79bae119e2005f5d766c8eabc03b16058da6e94300110d029157e7e95 + checksum: 10/7c3c71e205aa748f3f3b24ca3a9bd9e07ee965175a0cdd672fca9604a97a668962190f9db80518ae94c6ce21f7b13b9dcadc37dea0d749b12988bb273c3f400a languageName: node linkType: hard -"@opentelemetry/instrumentation-cucumber@npm:^0.24.0": - version: 0.24.0 - resolution: "@opentelemetry/instrumentation-cucumber@npm:0.24.0" +"@opentelemetry/instrumentation-cucumber@npm:^0.29.0": + version: 0.29.0 + resolution: "@opentelemetry/instrumentation-cucumber@npm:0.29.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/fc9542beb91284eb42cd72fb5d18b8a183ccdc7b40ca1c5f04013aded6517a336544d6e83fadfff51dfffb2abf748f0dc546eddb67330bed773d7275038697df + checksum: 10/03498a50448ea0ce3a7e96db4903697791b13b0bb42a933a92dffb4f499acaceb7cc899365c8208d12107d1fc34b65b19be3b1d03184c312b1358ad629ec8b55 languageName: node linkType: hard -"@opentelemetry/instrumentation-dataloader@npm:^0.26.1": - version: 0.26.1 - resolution: "@opentelemetry/instrumentation-dataloader@npm:0.26.1" +"@opentelemetry/instrumentation-dataloader@npm:^0.30.0": + version: 0.30.0 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.30.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/b2ca71ec08651a193c3291cf80b502c8a8043790fba787f13bd7f920fdb1ba976ec39fd23035ae833bdb877b97da7194bf7ae98fa07c30c1f1006da5e1bca111 + checksum: 10/9c45ba18866980e898fd8a7060d655e5c37dfa7dd5878a56e6ba5de22e88273215fc4fa02f8971b403abfeb79fa28dc938d5ca071ef812526dedd3774caf8587 languageName: node linkType: hard -"@opentelemetry/instrumentation-dns@npm:^0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-dns@npm:0.52.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/a6ff78cacbcc50ff3d4f805144223a0fc2fa7df2bea5c198511c2e1a546550dddef75f8b85b5ed78bf5ffa50d006e166ae1f19ab6a4125edbdab7cc92cb49bad - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-express@npm:^0.57.1": - version: 0.57.1 - resolution: "@opentelemetry/instrumentation-express@npm:0.57.1" - dependencies: - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/0d70672aa6c26b46fbc219c9b0dbc08e5b1f5614d7d25590c1ba8149054552a2226edbf3c7287a0ccba49d6a638379048709ecfbb175327a7bec72b4113d5d5e - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-fastify@npm:^0.53.1": - version: 0.53.1 - resolution: "@opentelemetry/instrumentation-fastify@npm:0.53.1" - dependencies: - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/e72e4e8adef7687519688bd2e2bd9ee49586cf1839aeabb1d916fe5308085cf7681536a93938d96a6f79b24d18e82c47faa4205ab2bdc7f435e708d42d626dbf - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-fs@npm:^0.28.0": - version: 0.28.0 - resolution: "@opentelemetry/instrumentation-fs@npm:0.28.0" - dependencies: - "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/f62c1256e3c47d958b296b57f8ab1d0b8b8851e9c3d6ad8691af514a33a2341ec1a9345d57a705a420a7a5757a297e03cc04c497a5701ac027a98b63b1a90ab1 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-generic-pool@npm:^0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.52.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/f94b27d84a4172f95f7a33fa967f2ad562d4dba3182b1e8b6658b4eceb8a26a678b407a232858378a034dd8ffdb4e66bd98b4a8861fe042be59af3e4b400244f - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-graphql@npm:^0.56.0": +"@opentelemetry/instrumentation-dns@npm:^0.56.0": version: 0.56.0 - resolution: "@opentelemetry/instrumentation-graphql@npm:0.56.0" + resolution: "@opentelemetry/instrumentation-dns@npm:0.56.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/7895e702484367ef9c26f7c79f0039ac9677133342253f3935e7203156cbc396dfd799ef84e4758e83c008ed8c6b6893a05fc1df2566cfaae65e3770485ad8c9 + checksum: 10/9cc729e47dd6e22e8c363eeb335fe9d90f98be4c81307bbbd1b504062a9b1a8e53189ae210490f222d10da15486e287796e8f69c40108914a746a7200fe90456 languageName: node linkType: hard -"@opentelemetry/instrumentation-grpc@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/instrumentation-grpc@npm:0.208.0" +"@opentelemetry/instrumentation-express@npm:^0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-express@npm:0.61.0" dependencies: - "@opentelemetry/instrumentation": "npm:0.208.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/039273c9985238ac74a3576c7a8e8be6b63a962f89e289eed2ee6eeca14821c82d384c6015b536bc42a082a39e5d4db10abcdaf47235e2ca0f2cc0ff47525b14 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fastify@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-fastify@npm:0.57.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/74fb7b4d01bbabe82da3a81c11bdbbb86a043f2eb47bbfac62791b0ee029d2d7613ec9d1e34aa0db884711f6d3b92391ad4eabffabaa7fa7c8ab042f0169a13f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fs@npm:^0.32.0": + version: 0.32.0 + resolution: "@opentelemetry/instrumentation-fs@npm:0.32.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/6a72169c4410700add4d2f7ff3c6e31e5a03040771932eb5392c0ab31c13b660a498727c876b95f0f8b88caa2d3af2b4c0d785bfc709c607b775f8797d76de85 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-generic-pool@npm:^0.56.0": + version: 0.56.0 + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.56.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.213.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/fae98b198728f06a323b552070c8f6b41a184e1e86c8f3a1868e3f100ac4e80d8985faa89a1ef42549d3e29d6ee9ec59781cfe49ddfddfa94de0e397da0cc46c + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-graphql@npm:^0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.61.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.213.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/ba5270bc5b98523d3865f277d5f45b077ab02b81e84f1d8ee34994ff5ac4d28b6810c87dd1f52daf57a4983929a09d6b02419a26b068a50990a34fa864e31aa5 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-grpc@npm:^0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/instrumentation-grpc@npm:0.213.0" + dependencies: + "@opentelemetry/instrumentation": "npm:0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/8f5a75c1cac44c2e15963b0c14259de7a3f0320391d24f4bbda2830e9471481af84be7f8b978c679d6dd94d5dfbb96bbe94c4dd601f70fb7ed2df10de89c195a + checksum: 10/6ac17dbed347571e954846c30b97d649800dab79718c04846813fc2358f6679e3050575801c0c5cbfda632220dfec673e4c695ca104d0e72637ce8604528959d languageName: node linkType: hard -"@opentelemetry/instrumentation-hapi@npm:^0.55.1": - version: 0.55.1 - resolution: "@opentelemetry/instrumentation-hapi@npm:0.55.1" +"@opentelemetry/instrumentation-hapi@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.59.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/af5e52ebcf56141c8973c334a1a5c4cfb01bf64974e6dd36502f93af4115500f70159492d19723201d30ad599885e87223f518d4598640a9f58d564ffb688d3b + checksum: 10/bcca71c35d525fe54abb3315cfad19dff635cdb1e72b7e15d083a468e05ae90eb966bbf1538bc4fdb0f16fb46057dd61780057c43fdcdb1a1ee5d94aa200cc27 languageName: node linkType: hard -"@opentelemetry/instrumentation-http@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/instrumentation-http@npm:0.208.0" +"@opentelemetry/instrumentation-http@npm:^0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/instrumentation-http@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/instrumentation": "npm:0.208.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/instrumentation": "npm:0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.29.0" forwarded-parse: "npm:2.1.2" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/f5e11eb7054d6701ea44ecb90321558e524b253a7945d1539f9971f83c5dfc1d294a6ac632eb35d2f19309837f16f6c405958c0bd9c50a8751dd07ee0704ca4e + checksum: 10/cbb8c2d639ddeff70f13a4781b2bb39067ce34ad9fd02a5f20f32865fb3b2e028c46dd29296a9190f42e1e7965877d9b73941f969de093c0d5e92ad0f4ae3aec languageName: node linkType: hard -"@opentelemetry/instrumentation-ioredis@npm:^0.57.0": - version: 0.57.0 - resolution: "@opentelemetry/instrumentation-ioredis@npm:0.57.0" +"@opentelemetry/instrumentation-ioredis@npm:^0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.61.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/redis-common": "npm:^0.38.2" "@opentelemetry/semantic-conventions": "npm:^1.33.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/01ae642771e62b7e460fdad4756ff1af503617b18af34bc16468b53d9afa674cdaa3a19c76b51ec1a7f05a7e838c8943a40ab13b09ee1f32c0cd02b5e0fb8f0d + checksum: 10/598d2a6763019a5b7b1c1139e12bb8199e7fc29aea54af3e78f77da9ea3ea45b98165581c74613923590ee242f6a90a30f5849b208f9e9268efdcc30d9a7d7cb languageName: node linkType: hard -"@opentelemetry/instrumentation-kafkajs@npm:^0.18.1": - version: 0.18.1 - resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.18.1" +"@opentelemetry/instrumentation-kafkajs@npm:^0.22.0": + version: 0.22.0 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.22.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.30.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5aeb751e2a7f5a6a602c4d92cebb9215b574ef50684b268183a7dc4e60d71f2568e4ac3fa9a258577799a8433306abcee3db25aaf9ee918202d6828b3aea8d37 + checksum: 10/ac0012cd95a29b7c29de27de57b1c3981de72a95e549fba99dc7ff3e1b1168a668b07f43cde32438890a150ad058af82063df233939338ecce05d60c1281bccf languageName: node linkType: hard -"@opentelemetry/instrumentation-knex@npm:^0.53.1": - version: 0.53.1 - resolution: "@opentelemetry/instrumentation-knex@npm:0.53.1" +"@opentelemetry/instrumentation-knex@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.57.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/a9acaaefcf1f4523b47b33f8e5bd99dfc9f803aa7c8c3fc79e3cc497a022edeefa890905708eccbecd0dff37b8cce5543a6d17c61e565d87bfa9bd1de43e1526 + checksum: 10/bf8a5b9030d51e50387c92ac91dcb1d8be6b829236a245ee834a9bd6c9206bf8f9ddf267e7f2ccb1095adc08ec28624144aae2e5a010523e7ef5479b978e3c49 languageName: node linkType: hard -"@opentelemetry/instrumentation-koa@npm:^0.57.1": - version: 0.57.1 - resolution: "@opentelemetry/instrumentation-koa@npm:0.57.1" +"@opentelemetry/instrumentation-koa@npm:^0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-koa@npm:0.61.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.36.0" peerDependencies: "@opentelemetry/api": ^1.9.0 - checksum: 10/8235d78b8c845ab86a264cd0882e6a95dc4ff890b13b02a81d0e3ccc9a73e055d732b14043cf2366940e1e808912520cf0ca96bc893a6aa89468a1e3a9846031 + checksum: 10/5426d58dd0c0498a5cdea2996a8840308ce9d7c48e8894f8df2abe9aa7fbd8b130a89d32c2465e660d19a9dc9772d4f74517da3d3702869c45ac0105d4877e65 languageName: node linkType: hard -"@opentelemetry/instrumentation-lru-memoizer@npm:^0.53.1": - version: 0.53.1 - resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.53.1" +"@opentelemetry/instrumentation-lru-memoizer@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.57.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/10a5840848ec59b658150af94dbb6297f5b8f9202b9bfe02a5f860540de92f1d38ec79a320a334a827cf06384f5f1bb19bb19c5d24bc3998808f933f05b41920 + checksum: 10/f4b41d072251be5d9a296a179b8ce09a734204bacacbb1f8e6f5ebc2e798a999ead9806a3944b203d35f50421e86f4f19069e0f2d211f37ac8c372cd89defbb3 languageName: node linkType: hard -"@opentelemetry/instrumentation-memcached@npm:^0.52.1": - version: 0.52.1 - resolution: "@opentelemetry/instrumentation-memcached@npm:0.52.1" +"@opentelemetry/instrumentation-memcached@npm:^0.56.0": + version: 0.56.0 + resolution: "@opentelemetry/instrumentation-memcached@npm:0.56.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.0" "@types/memcached": "npm:^2.2.6" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/b61f3a67b86e2e37b565c85c23c9161bce8ed634bb463e9dc58b409c3b512e8da04b896ca62caebca918d81e8496fc29e7b98a5e1a839f1f992b623b069e045e + checksum: 10/9c3cf65410c232d61edc1e8ea95c83927d77554b269e3bdbeef31bad6ed03387f7ce838ad5968af20c8abca140730005ad68505f6a36a6d976dae354c88c07d9 languageName: node linkType: hard -"@opentelemetry/instrumentation-mongodb@npm:^0.62.0": - version: 0.62.0 - resolution: "@opentelemetry/instrumentation-mongodb@npm:0.62.0" +"@opentelemetry/instrumentation-mongodb@npm:^0.66.0": + version: 0.66.0 + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.66.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/994a4a020f1876b3d4692e60c80c1dcb28e5cc5bfe91d270f5b4267afc03d3e5e43a8a05c69bc568b3ec91fa6a35fbbac52cb238fbd5f1a86ab7c07782d79dd3 + checksum: 10/50baa175386bc71daf9098ddb39460bde17fac1a7c5bc347f07fd3e0a752dc1c56cc1cd5a09048b51892b1cb29b244d4865da8deec34f3ea6c645e6e97d93b9c languageName: node linkType: hard -"@opentelemetry/instrumentation-mongoose@npm:^0.55.1": - version: 0.55.1 - resolution: "@opentelemetry/instrumentation-mongoose@npm:0.55.1" +"@opentelemetry/instrumentation-mongoose@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.59.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/6442ce148c930f7eb4b982cf2085e2f84f79a9d429d5c2162bd95d05716abe5b24414a923a6c69eaca5faf83b7cf97480ae92523e8d0149430299dee6fd98be3 + checksum: 10/121506d6e4941145c7066b2b723eaa2e0182a5d0b565d61cbf6269454d86c180ec417c66cf4320ee82d173099d59b098c924bb17e72a873722aa54f14aaa5e7e languageName: node linkType: hard -"@opentelemetry/instrumentation-mysql2@npm:^0.55.1": - version: 0.55.1 - resolution: "@opentelemetry/instrumentation-mysql2@npm:0.55.1" +"@opentelemetry/instrumentation-mysql2@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.59.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.0" "@opentelemetry/sql-common": "npm:^0.41.2" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/1bbe617aff0bba4807ab52d8dd5af0c031412f1e8ef3d7180dd9eb3645451ee4bf587c7a0140a1c22c48777c394778e7a387992b391eb938a7b8bbb818527c64 + checksum: 10/7fa1941bdad8cbfcf6084ba2a291c68d101050c738bad06915f2cce4d475548353fdd99ba8f41bcc26267cd092eaa485849343256bd48043bc2dfa0412570f6b languageName: node linkType: hard -"@opentelemetry/instrumentation-mysql@npm:^0.55.0": - version: 0.55.0 - resolution: "@opentelemetry/instrumentation-mysql@npm:0.55.0" +"@opentelemetry/instrumentation-mysql@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.59.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.0" "@types/mysql": "npm:2.15.27" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/492de2b5d4759119935a167cbc549f5d4ba1373b43bb39cbabadee896fc3b5256cd2bd9b08abe0704f5bbaa29afeea77a16186533e06449878fefdaa05aa8e98 + checksum: 10/87833280fa53a691d95263066b04458db33b6bb49a15fb97add7505fcae0cb574da5ac9e53c7788d2044ae108cbc42308eb2684c2fa05751098cb54d6bd0d315 languageName: node linkType: hard -"@opentelemetry/instrumentation-nestjs-core@npm:^0.55.0": - version: 0.55.0 - resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.55.0" +"@opentelemetry/instrumentation-nestjs-core@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.59.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.30.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/6c930d4b670f3a1764d11539867656594fa1a74d47597ca0b4fdf0425403db14552cf41a28ae4169d2034680f9d968a1c90cb5bfe2a6504bf101eaef026cc2fe + checksum: 10/4a86f6658e45993381051352c61ef904899675c641210bd30210298c86585881212b4db3fe769ef7bbe05dc20c4a2019a6ecddd6d7ddfb10b01cb330bc727b43 languageName: node linkType: hard -"@opentelemetry/instrumentation-net@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-net@npm:0.53.0" +"@opentelemetry/instrumentation-net@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-net@npm:0.57.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/3fb1868fe15916a8806279ae8da6e57a4e8aff2637bdca608a0e11000eaf93ee9af092f88eef20bdc76e77cdf713ce767ba11b2691146a9375fb1fa8b735fd08 + checksum: 10/7aff49402d16ce3d501060759d1c729a5e2d514f5ebb8a39a0190f828a3b402886aa2cefa4012bc93f3a1c002b67b4729ed193905c67556fbebb2863ae18370a languageName: node linkType: hard -"@opentelemetry/instrumentation-openai@npm:^0.7.1": - version: 0.7.1 - resolution: "@opentelemetry/instrumentation-openai@npm:0.7.1" +"@opentelemetry/instrumentation-openai@npm:^0.11.0": + version: 0.11.0 + resolution: "@opentelemetry/instrumentation-openai@npm:0.11.0" dependencies: - "@opentelemetry/api-logs": "npm:^0.208.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/api-logs": "npm:^0.213.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.36.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/f7c5fcf0467e17aebe87c2cc0fb46b8d7a7601cfb31ae0378a30bdafa1c9cf7e8f203d8d8e5f78e41705a9fdca245fa3ba401585a9ce21f1f04f041a0616087c + checksum: 10/8cfa799a96b9d815f7a2c1ab9c68251826077ee9fa77f974bcf031490acb432095f280e17745e14eefae60782a12d3dbebbb8798889873ed6c4c9e7eae72f650 languageName: node linkType: hard -"@opentelemetry/instrumentation-oracledb@npm:^0.34.1": - version: 0.34.1 - resolution: "@opentelemetry/instrumentation-oracledb@npm:0.34.1" +"@opentelemetry/instrumentation-oracledb@npm:^0.38.0": + version: 0.38.0 + resolution: "@opentelemetry/instrumentation-oracledb@npm:0.38.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.34.0" "@types/oracledb": "npm:6.5.2" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/c00983a5d03b1611fa8e7c17d82479c1c43e295559f2c616435c569059d23ece026e0c83dd9f130aff7d9436a66ec32766bf04d106d98c6f0439cf2c9ee2df6b + checksum: 10/5ca691e164a4fc8ef686553e062e67342d7e929484bdb6f2adef219e1e50d55a6dd5570381a4347345e1fd514d5667cbe29a2b7bb9801eda77bd532b04341307 languageName: node linkType: hard -"@opentelemetry/instrumentation-pg@npm:^0.61.2": - version: 0.61.2 - resolution: "@opentelemetry/instrumentation-pg@npm:0.61.2" +"@opentelemetry/instrumentation-pg@npm:^0.65.0": + version: 0.65.0 + resolution: "@opentelemetry/instrumentation-pg@npm:0.65.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.34.0" "@opentelemetry/sql-common": "npm:^0.41.2" "@types/pg": "npm:8.15.6" - "@types/pg-pool": "npm:2.0.6" + "@types/pg-pool": "npm:2.0.7" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/74359ac9a5130096f5b43040f4dc10d6b2d6f104099b5244106fee6c3ffd3a2dc729ad65e9c0447c8637607de156de28f12dbf9431226dc50e2ba3b9ed7d7e29 + checksum: 10/835953ae633fe0efa3ad0a39579f7860c59ec066330a592682ceea0eb14eb770d94c309a4fd64ff2162e804fe53cfcb38f1fd3d19116786473cfd8e6a1eaee7f languageName: node linkType: hard -"@opentelemetry/instrumentation-pino@npm:^0.55.1": - version: 0.55.1 - resolution: "@opentelemetry/instrumentation-pino@npm:0.55.1" +"@opentelemetry/instrumentation-pino@npm:^0.59.0": + version: 0.59.0 + resolution: "@opentelemetry/instrumentation-pino@npm:0.59.0" dependencies: - "@opentelemetry/api-logs": "npm:^0.208.0" + "@opentelemetry/api-logs": "npm:^0.213.0" "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/7da10316dfe60622e6080a54112ebeea7873b43d48c32910108512db07dbc292cd785fa2be2d53a9b05d31f0649f62f2f01aba9f2795949fd6582b37b5236cf4 + checksum: 10/9198c2bc9ab17fabef251d69feb9f3199b39e878792a9a9fb5b7e0936faf860d4706170db8578399bcb7e8826d7d544199dfeabec16cfb4b55df02a537f8c1e9 languageName: node linkType: hard -"@opentelemetry/instrumentation-redis@npm:^0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation-redis@npm:0.57.2" +"@opentelemetry/instrumentation-redis@npm:^0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-redis@npm:0.61.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/redis-common": "npm:^0.38.2" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/8e1e7840f1b524d0d69a0783216173f8dfc794133385f7449b5215e5a04775d469a009ecef02d99fe05fa0382859a25e65653eadbde1fece3514ba119d1db1a1 + checksum: 10/667eacf85fcf3de51e5052f66826aed90b62e565099c7cea6b465830ee0fd9f149a7ddf208a00e34f3bbb65213ed9d24b1601c242234e78db21447f9f9fd56fe languageName: node linkType: hard -"@opentelemetry/instrumentation-restify@npm:^0.54.0": - version: 0.54.0 - resolution: "@opentelemetry/instrumentation-restify@npm:0.54.0" +"@opentelemetry/instrumentation-restify@npm:^0.58.0": + version: 0.58.0 + resolution: "@opentelemetry/instrumentation-restify@npm:0.58.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5488aaf97dcc5e1ec8f9c055258c587553f20f825b8d7c5dcd2a7398adf85554f1d8b5d1c35235c98280ab6662917b12d148215c4d3882e205314883c9bed0b8 + checksum: 10/c6b1aaa19e9d4f2520a4d2ac61b6125b2abafa5bba81331fc263b59e5256d2ce8b016dc8cd12e2ac1fcebd6068844d894450f45aa2ed7dc4bb051916d055954d languageName: node linkType: hard -"@opentelemetry/instrumentation-router@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-router@npm:0.53.0" +"@opentelemetry/instrumentation-router@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-router@npm:0.57.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/3a24e2cd25b0bff9e96c1956fe9bb2fbda8aa807125cb58535fb16e4e10a65ac391a2db748bc50a1ca7e35fcfbc26e078a3fc96e0f105f813b06f57079f84062 + checksum: 10/71b0c108708ac214377e96769c7863f3172e17a8a24afd2dc82ac48aebd4f0bf9ed73266b7549ac9d05221df66af35e9bd6a493f8fd546cd59f52b24f8bfae5b languageName: node linkType: hard -"@opentelemetry/instrumentation-runtime-node@npm:^0.22.0": - version: 0.22.0 - resolution: "@opentelemetry/instrumentation-runtime-node@npm:0.22.0" +"@opentelemetry/instrumentation-runtime-node@npm:^0.26.0": + version: 0.26.0 + resolution: "@opentelemetry/instrumentation-runtime-node@npm:0.26.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/20a45189e0bffb08e9ef111fc4be33e98309948aaa10b448979bd900b0c53a752606366b44fa2351ba2fdc2cf775ba4eaf03cb038ba31798c679180ea987dba8 + checksum: 10/b3ea3c4dece7dc3d35c2dc7de38cccc3b73c34175c4fb28a8442f72b312b7f83417d37c9f1864feb02ed2c8d8940f0525fe0e2e0c7d0615bcfe77825b8125928 languageName: node linkType: hard -"@opentelemetry/instrumentation-socket.io@npm:^0.55.1": - version: 0.55.1 - resolution: "@opentelemetry/instrumentation-socket.io@npm:0.55.1" +"@opentelemetry/instrumentation-socket.io@npm:^0.60.0": + version: 0.60.0 + resolution: "@opentelemetry/instrumentation-socket.io@npm:0.60.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/6df6e677ee49a65622a659217d117a81a8dae89e7b5cb283e34da112697179f892842f17ceb00a86c62b9d07ec5d8b49bd334062f582084d779c8243fc904fa0 + checksum: 10/7bc82b036ad37fc1361e7f44d97036e8e456f27e133139d21bbdd1194d32ff9ff3a8aa7101874d966e44ac39472e5ef339145a372e8338690ba65a18aaa4b606 languageName: node linkType: hard -"@opentelemetry/instrumentation-tedious@npm:^0.28.0": - version: 0.28.0 - resolution: "@opentelemetry/instrumentation-tedious@npm:0.28.0" +"@opentelemetry/instrumentation-tedious@npm:^0.32.0": + version: 0.32.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.32.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.33.0" "@types/tedious": "npm:^4.0.14" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/43b1bd400aa393ea2000ea2c6f40fee40a06184739d014175f2713eb8d284a5b4eb7c047801084506fa1fed7d7a55d25dfb009592d2ebc193370c10fb3d3df13 + checksum: 10/25b7c42e75ee393990ff1502ec7e4dea33e9615b35ff32bded2634824c0278dc55cccd6b0d6dbecdfff91d3f998be5d9e89526469287ea5df4637810dfbfc1c6 languageName: node linkType: hard -"@opentelemetry/instrumentation-undici@npm:^0.19.0": - version: 0.19.0 - resolution: "@opentelemetry/instrumentation-undici@npm:0.19.0" +"@opentelemetry/instrumentation-undici@npm:^0.23.0": + version: 0.23.0 + resolution: "@opentelemetry/instrumentation-undici@npm:0.23.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" "@opentelemetry/semantic-conventions": "npm:^1.24.0" peerDependencies: "@opentelemetry/api": ^1.7.0 - checksum: 10/862ea5e49c2cf38c7a135f5ea6a95f1b8ca009620f9b0e4d6c7ffb69e8e1d4e7d4169e4cf0cd2af4ce574864b37ea3f7c039b0e6600d3e490f3c263a8647325e + checksum: 10/bfb6ed86e8098827725f5cb2f21e21d5c0b13254ab6da84418de68b3c1cadc2f503e4fc213cd6f2a661819e8327c9af9b87c32468444a772e8b82b966cb616a6 languageName: node linkType: hard -"@opentelemetry/instrumentation-winston@npm:^0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-winston@npm:0.53.0" +"@opentelemetry/instrumentation-winston@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-winston@npm:0.57.0" dependencies: - "@opentelemetry/api-logs": "npm:^0.208.0" - "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/api-logs": "npm:^0.213.0" + "@opentelemetry/instrumentation": "npm:^0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5c3efd7c9cb074718cf4de99c27acf69bbb35bdf16d74f92dbdd8fd4efba2c637e3b234155e335b473d5caa060a6f40a49136d808017547d3a4d5eab66eed9b7 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation@npm:0.208.0, @opentelemetry/instrumentation@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/instrumentation@npm:0.208.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - import-in-the-middle: "npm:^2.0.0" - require-in-the-middle: "npm:^8.0.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/0591121c1bab29b8246ba879b1ed91f2db17680cfce56a635bf2e81390a9140f029b094ff4498ff154132379192bf424d7d234d2114883735603e4a6581a4a79 + checksum: 10/0972e3ac889d5408687ddb139c49f69de78b6eebba30bdade3b4b605a240248523971ffd987b9bec976c7dba4dc1f67dfef63fe145937ee9061e4c0b15daa7f6 languageName: node linkType: hard @@ -14076,15 +14146,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.208.0" +"@opentelemetry/instrumentation@npm:0.213.0, @opentelemetry/instrumentation@npm:^0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/instrumentation@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" + "@opentelemetry/api-logs": "npm:0.213.0" + import-in-the-middle: "npm:^3.0.0" + require-in-the-middle: "npm:^8.0.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/d24e1e766a8059861232fd338be7d65bded5167176b4c7c1be9a1833167f73fd352392a1df28ca002734bdbe00a70d06d1e7daeb97d9d7f71f85dc310a831a42 + checksum: 10/69baeaae0c5836ede140485530954d32c8d20d864340f7d57b43a6e3ef9c10394d29a1884dd2b076512aec896039a1ea02f698282649b5aa3fa59b13bca00f97 languageName: node linkType: hard @@ -14100,17 +14171,15 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-grpc-exporter-base@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.208.0" +"@opentelemetry/otlp-exporter-base@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.213.0" dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/otlp-exporter-base": "npm:0.208.0" - "@opentelemetry/otlp-transformer": "npm:0.208.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/f88ee3d2377a27ee41077b5cb79b122ee8f8fa95cab9457ca53f8ef9b92b688b3c455b05c80b55ff3bf23d254eb596c361089fdf4a84b4f0317fc7e059fb4790 + checksum: 10/ff36dbdae9ae3b4b7e25fb650e9b351851ae9d50cd09b90d2be709a10dd62a8fef522adad9b27010825e9d22d96238ab37f38530cf92eaf3340d2730d38ad301 languageName: node linkType: hard @@ -14128,20 +14197,17 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.208.0" +"@opentelemetry/otlp-grpc-exporter-base@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.213.0" dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - protobufjs: "npm:^7.3.0" + "@grpc/grpc-js": "npm:^1.14.3" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/otlp-exporter-base": "npm:0.213.0" + "@opentelemetry/otlp-transformer": "npm:0.213.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/867a16a7a723a3df7a7ea8fa9f3c976139e32954efd1146812486cbee20c5eea73ba03841c64312b049e998a7e678589ddef80f7a04aaf95f649753eebe79e3d + checksum: 10/dc6aa2414b4f24349a749423504dc0aba00164241b30bb468cfab39b7936408fb00ede7fa675ea95ac131249764a1320a8657765c4509d0d93245879a49905f9 languageName: node linkType: hard @@ -14162,14 +14228,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/propagator-b3@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/propagator-b3@npm:2.2.0" +"@opentelemetry/otlp-transformer@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/api-logs": "npm:0.213.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-logs": "npm:0.213.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" + protobufjs: "npm:^7.0.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/567a1bdd74cd81008fc5e4289c5a870daf5c46326a7c21e9dfd7e462e42c033dd51e7acd8c7b206926b5e78d1a4e3d72d3b13c1ce290ecec984730e847d4e122 + "@opentelemetry/api": ^1.3.0 + checksum: 10/3f4caa24f9b4d57d9290f9090701031641e8aff33bca13711e2a2766c1acd8ee3c348342f9d81fcb73bbd48fe910444c10990b148d70437f0508bb8ad3751b1b languageName: node linkType: hard @@ -14184,14 +14256,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/propagator-jaeger@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/propagator-jaeger@npm:2.2.0" +"@opentelemetry/propagator-b3@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/propagator-b3@npm:2.6.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/7da76cab4387cd52723865ccee055dafeed1763c784ea6e13152f1264ad3ccd5b0cace442dac00755eeb17d9af216d0e235aeb6c739a9703e1b1d14b3bf1f5f8 + checksum: 10/8cc50658cc5a31a06ae97b15cda6f0d27fe8e19a06b841ff07b84e458f2e9d81347bbff823819eb8fccd387335475091d0834b9bbe1295b77a1d6a253fcd522d languageName: node linkType: hard @@ -14206,6 +14278,17 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/propagator-jaeger@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/propagator-jaeger@npm:2.6.0" + dependencies: + "@opentelemetry/core": "npm:2.6.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10/d9f6f971994aa4cbf5495368e17e248631fbf3c3eeb2405f0c7ce987c830388e6d6987a5f85ba24cfbf1cacdff204cf1b8338703c4845158f602407ec99b64f0 + languageName: node + linkType: hard + "@opentelemetry/redis-common@npm:^0.38.2": version: 0.38.2 resolution: "@opentelemetry/redis-common@npm:0.38.2" @@ -14213,82 +14296,70 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.32.0": - version: 0.32.0 - resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.32.0" +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.33.3": + version: 0.33.3 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.33.3" dependencies: "@opentelemetry/core": "npm:^2.0.0" "@opentelemetry/resources": "npm:^2.0.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/2c3825e5bd87f763a17b660e6acc4b6aeee3e3ebe9c90a3f0d09010153814724aa46f3af3e57e340eb971bdda5503a15cd823f87311c911f671acdbbf14ccbb2 + checksum: 10/600d67e1a910d8278a97171a1a5bf403da34ca070348cb8f7af09b19bedfd48464dc8c794775723058b60137c9a48cb3b7894e6afb81ae62e99be9b4b24f4f62 languageName: node linkType: hard -"@opentelemetry/resource-detector-aws@npm:^2.9.0": - version: 2.9.0 - resolution: "@opentelemetry/resource-detector-aws@npm:2.9.0" +"@opentelemetry/resource-detector-aws@npm:^2.13.0": + version: 2.13.0 + resolution: "@opentelemetry/resource-detector-aws@npm:2.13.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" "@opentelemetry/resources": "npm:^2.0.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/925be76939c0b8b786e80e17d9be62b42c7cdcf672d7f10f21c88786510fba114ff53fc904c2c9be2a429704d8bcb656898f7a2f267d96f5bc849c5b92584517 + checksum: 10/b0d057684f83ca666b3ac61d04a53e0129c3a434b38e3870db071133cc9d3552c8ff9f9b50e9f326f1a450ffd22af292ba4fc594cf429207966ce00a89286ba4 languageName: node linkType: hard -"@opentelemetry/resource-detector-azure@npm:^0.17.0": - version: 0.17.0 - resolution: "@opentelemetry/resource-detector-azure@npm:0.17.0" +"@opentelemetry/resource-detector-azure@npm:^0.21.0": + version: 0.21.0 + resolution: "@opentelemetry/resource-detector-azure@npm:0.21.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" "@opentelemetry/resources": "npm:^2.0.0" "@opentelemetry/semantic-conventions": "npm:^1.37.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/2da1ed2f0dd4e1f8fc2eaeb90956cb97055b30ecf6b42cac418f014b1b1388b80ba9933663ba8f85ae2a2bc7406b241d9e5f430cf1ddbd351e0f70496b8ada60 + checksum: 10/cfc4f8e419ec7b077c87599d0d842aeb741413f33e66e97ab093f79b292eaa9eb8dc54ea4e644550f31363ebf4efc8182c03df302143d6e89552427f575e2177 languageName: node linkType: hard -"@opentelemetry/resource-detector-container@npm:^0.8.0": - version: 0.8.0 - resolution: "@opentelemetry/resource-detector-container@npm:0.8.0" +"@opentelemetry/resource-detector-container@npm:^0.8.4": + version: 0.8.4 + resolution: "@opentelemetry/resource-detector-container@npm:0.8.4" dependencies: "@opentelemetry/core": "npm:^2.0.0" "@opentelemetry/resources": "npm:^2.0.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/aa621d823c70ed09a9601b7167897f40f94ad2514512208db2ded9dd5f307e8dfe06e9a51347d912bd4367031d0c09cab2642157ebe266b45432a6038d964fe9 + checksum: 10/65fb62e9e414ac941e94f5befeaca5fd2d2e475e414c9e4521b66729c6a2d9fc3b6e32bb316f95459c6c12910feaa124498827faa466017bd0d3004a26a6630a languageName: node linkType: hard -"@opentelemetry/resource-detector-gcp@npm:^0.44.0": - version: 0.44.0 - resolution: "@opentelemetry/resource-detector-gcp@npm:0.44.0" +"@opentelemetry/resource-detector-gcp@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/resource-detector-gcp@npm:0.48.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" "@opentelemetry/resources": "npm:^2.0.0" - gcp-metadata: "npm:^6.0.0" + gcp-metadata: "npm:^8.0.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10/cf7b401a9dc77a87793db04e9b92e0822562d703b270957a482c6cb41fbbcf3b7cb35089f7273c6143cd9c78ea45d849fc7c54ed9b29435e4b384ad3b5b05cb9 + checksum: 10/2c11431c7be28620868d091c4bf9d646ce524587056e94c04b8d9b31bdec77f54ca132a1ac8ff1e6f894009d8cb776527eb3639d3f31b11eeee576b736b3d56a languageName: node linkType: hard -"@opentelemetry/resources@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/resources@npm:2.2.0" - dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10/65ccdb1de957dc89aef252cf84b73cd0257ec44feec2b513fcf08e8c4d03e97275661d3f60c4b6134cee33ca4359a5ab6ef5d3a97339a3585aa997a381ef9098 - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:2.5.0, @opentelemetry/resources@npm:^2.0.0": +"@opentelemetry/resources@npm:2.5.0": version: 2.5.0 resolution: "@opentelemetry/resources@npm:2.5.0" dependencies: @@ -14300,16 +14371,15 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/sdk-logs@npm:0.208.0" +"@opentelemetry/resources@npm:2.6.0, @opentelemetry/resources@npm:^2.0.0": + version: 2.6.0 + resolution: "@opentelemetry/resources@npm:2.6.0" dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10/8413cdbf3a072d79a569ca7bcf3c8b333dfb1cb11a7a8841a9bb5482d6ee58d5a3a26efa0b9f02bf410b3f9fa99995ee5aa58aab505de560a2b2ac0b114ce70d + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10/837e76911d013e52c1c0cd8da6f2912818fcc107fd1c6fcb2ec8faa6e617b802e0eef1aa53bd4417c7a77c96ea02b6f5bbdccb9ff411ef8efeff49c2e02d9443 languageName: node linkType: hard @@ -14326,15 +14396,17 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-metrics@npm:2.2.0" +"@opentelemetry/sdk-logs@npm:0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/sdk-logs@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" + "@opentelemetry/api-logs": "npm:0.213.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.9.0 <1.10.0" - checksum: 10/d6dacce73319e038d55a67f5b1a7a153531a703ef881b03df52f2d76685a4d53d0d840e02a0e0b24eddae4bd7d11c694e3c146f8db78e19d353316372d04c065 + "@opentelemetry/api": ">=1.4.0 <1.10.0" + checksum: 10/c443b0f2c88582713c291ca86106679ffdadee96ea428b5058e0cb7470b387828e78d6614e7e01e5ad82f4c44571c1080aa2cf69e621bd2e07089105794cacdf languageName: node linkType: hard @@ -14350,35 +14422,15 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-node@npm:^0.208.0": - version: 0.208.0 - resolution: "@opentelemetry/sdk-node@npm:0.208.0" +"@opentelemetry/sdk-metrics@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/sdk-metrics@npm:2.6.0" dependencies: - "@opentelemetry/api-logs": "npm:0.208.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/exporter-logs-otlp-grpc": "npm:0.208.0" - "@opentelemetry/exporter-logs-otlp-http": "npm:0.208.0" - "@opentelemetry/exporter-logs-otlp-proto": "npm:0.208.0" - "@opentelemetry/exporter-metrics-otlp-grpc": "npm:0.208.0" - "@opentelemetry/exporter-metrics-otlp-http": "npm:0.208.0" - "@opentelemetry/exporter-metrics-otlp-proto": "npm:0.208.0" - "@opentelemetry/exporter-prometheus": "npm:0.208.0" - "@opentelemetry/exporter-trace-otlp-grpc": "npm:0.208.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:0.208.0" - "@opentelemetry/exporter-trace-otlp-proto": "npm:0.208.0" - "@opentelemetry/exporter-zipkin": "npm:2.2.0" - "@opentelemetry/instrumentation": "npm:0.208.0" - "@opentelemetry/propagator-b3": "npm:2.2.0" - "@opentelemetry/propagator-jaeger": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" - "@opentelemetry/sdk-logs": "npm:0.208.0" - "@opentelemetry/sdk-metrics": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" - "@opentelemetry/sdk-trace-node": "npm:2.2.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10/929be561500feb30329314ee0f42adc064f0d9def9bf112b11c0f27ed3848366f172a13b178adf56fe96037f9e886c80a46122f105c2756a1fd1f4fdb0f831c2 + "@opentelemetry/api": ">=1.9.0 <1.10.0" + checksum: 10/5fd1254ab86cdb6573999f3c5d60b8332fb3a2b7d50d1befcfd9d8ef021d5e9e405e31394f7aa4a106a4546bd6308a652a78cd9a35d6fbe56e44984d605cb5d5 languageName: node linkType: hard @@ -14416,16 +14468,37 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-trace-base@npm:2.2.0" +"@opentelemetry/sdk-node@npm:^0.213.0": + version: 0.213.0 + resolution: "@opentelemetry/sdk-node@npm:0.213.0" dependencies: - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/resources": "npm:2.2.0" + "@opentelemetry/api-logs": "npm:0.213.0" + "@opentelemetry/configuration": "npm:0.213.0" + "@opentelemetry/context-async-hooks": "npm:2.6.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/exporter-logs-otlp-grpc": "npm:0.213.0" + "@opentelemetry/exporter-logs-otlp-http": "npm:0.213.0" + "@opentelemetry/exporter-logs-otlp-proto": "npm:0.213.0" + "@opentelemetry/exporter-metrics-otlp-grpc": "npm:0.213.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.213.0" + "@opentelemetry/exporter-metrics-otlp-proto": "npm:0.213.0" + "@opentelemetry/exporter-prometheus": "npm:0.213.0" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:0.213.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:0.213.0" + "@opentelemetry/exporter-trace-otlp-proto": "npm:0.213.0" + "@opentelemetry/exporter-zipkin": "npm:2.6.0" + "@opentelemetry/instrumentation": "npm:0.213.0" + "@opentelemetry/propagator-b3": "npm:2.6.0" + "@opentelemetry/propagator-jaeger": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/sdk-logs": "npm:0.213.0" + "@opentelemetry/sdk-metrics": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" + "@opentelemetry/sdk-trace-node": "npm:2.6.0" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10/0838128f965055b5f8d37026a2f4736ebb77a772a94b9f5b7accb0447a44cfa279da4da959a82565e958e1676ad2a02c17f6fd0e688b205bdde0c846e310c643 + checksum: 10/2175a8ba71d3657b2a19972a2f45cca34c908e710b16b27aec1beb918c39b64170ade4c2c2059b49aeb3726044e176ce2caa4f4c6349af19ba8c74401601c843 languageName: node linkType: hard @@ -14442,16 +14515,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-node@npm:2.2.0": - version: 2.2.0 - resolution: "@opentelemetry/sdk-trace-node@npm:2.2.0" +"@opentelemetry/sdk-trace-base@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/sdk-trace-base@npm:2.6.0" dependencies: - "@opentelemetry/context-async-hooks": "npm:2.2.0" - "@opentelemetry/core": "npm:2.2.0" - "@opentelemetry/sdk-trace-base": "npm:2.2.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/resources": "npm:2.6.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/4918bc6a66649203d7165a8beca5a3ebaf05d91a2b4910aa195f582a6b50658e86303e00a4fc53172e48cac3cc79b50906d70a3b989895b423dc90dad5e9b5da + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10/8ca3c1c4d7a95ec8a28ab5237162e31334216a59408e9d9d10ad51f5709911a405699ad69f445c212aad55fb6cc2c70f473b835e9e52bf4ed63f237c4d1813af languageName: node linkType: hard @@ -14468,6 +14541,19 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/sdk-trace-node@npm:2.6.0": + version: 2.6.0 + resolution: "@opentelemetry/sdk-trace-node@npm:2.6.0" + dependencies: + "@opentelemetry/context-async-hooks": "npm:2.6.0" + "@opentelemetry/core": "npm:2.6.0" + "@opentelemetry/sdk-trace-base": "npm:2.6.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10/b1576f44198ae18ab36dea92e5c254eb6a96ca5793a5adbb96a01c586dd992e3532eb291c9ea225e2cd6979f7f6dd3fca2fad56ab360ba573f8b3bfb462dd2aa + languageName: node + linkType: hard + "@opentelemetry/semantic-conventions@npm:1.28.0": version: 1.28.0 resolution: "@opentelemetry/semantic-conventions@npm:1.28.0" @@ -14500,144 +14586,144 @@ __metadata: languageName: node linkType: hard -"@oxc-resolver/binding-android-arm-eabi@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.16.3" +"@oxc-resolver/binding-android-arm-eabi@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-android-arm64@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-android-arm64@npm:11.16.3" +"@oxc-resolver/binding-android-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-android-arm64@npm:11.19.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-arm64@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.16.3" +"@oxc-resolver/binding-darwin-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-darwin-arm64@npm:11.19.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-darwin-x64@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-darwin-x64@npm:11.16.3" +"@oxc-resolver/binding-darwin-x64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-darwin-x64@npm:11.19.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-freebsd-x64@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.16.3" +"@oxc-resolver/binding-freebsd-x64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-freebsd-x64@npm:11.19.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.16.3" +"@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm-gnueabihf@npm:11.19.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.16.3" +"@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm-musleabihf@npm:11.19.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-gnu@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.16.3" +"@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm64-gnu@npm:11.19.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-arm64-musl@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.16.3" +"@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-arm64-musl@npm:11.19.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.16.3" +"@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-ppc64-gnu@npm:11.19.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.16.3" +"@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-riscv64-gnu@npm:11.19.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-riscv64-musl@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.16.3" +"@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-riscv64-musl@npm:11.19.1" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-linux-s390x-gnu@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.16.3" +"@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-s390x-gnu@npm:11.19.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-gnu@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.16.3" +"@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-x64-gnu@npm:11.19.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-resolver/binding-linux-x64-musl@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.16.3" +"@oxc-resolver/binding-linux-x64-musl@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-linux-x64-musl@npm:11.19.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-resolver/binding-openharmony-arm64@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.16.3" +"@oxc-resolver/binding-openharmony-arm64@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-openharmony-arm64@npm:11.19.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-wasm32-wasi@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.16.3" +"@oxc-resolver/binding-wasm32-wasi@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-wasm32-wasi@npm:11.19.1" dependencies: "@napi-rs/wasm-runtime": "npm:^1.1.1" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-resolver/binding-win32-arm64-msvc@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.16.3" +"@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-arm64-msvc@npm:11.19.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-resolver/binding-win32-ia32-msvc@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-win32-ia32-msvc@npm:11.16.3" +"@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-ia32-msvc@npm:11.19.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxc-resolver/binding-win32-x64-msvc@npm:11.16.3": - version: 11.16.3 - resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.16.3" +"@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1": + version: 11.19.1 + resolution: "@oxc-resolver/binding-win32-x64-msvc@npm:11.19.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -14964,993 +15050,1028 @@ __metadata: languageName: node linkType: hard -"@radix-ui/primitive@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/primitive@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" - checksum: 10/2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1 +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10/ee27abbff0d6d305816e9314655eb35e72478ba47416bc9d5cb0581728be35e3408cfc0748313837561d635f0cb7dfaae26e61831f0e16c0fd7d669a612f2cb0 languageName: node linkType: hard -"@radix-ui/react-arrow@npm:1.0.3": - version: 1.0.3 - resolution: "@radix-ui/react-arrow@npm:1.0.3" +"@radix-ui/react-arrow@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-arrow@npm:1.1.7" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-primitive": "npm:2.1.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/8cca086f0dbb33360e3c0142adf72f99fc96352d7086d6c2356dbb2ea5944cfb720a87d526fc48087741c602cd8162ca02b0af5e6fdf5f56d20fddb44db8b4c3 + checksum: 10/6cdf74f06090f8994cdf6d3935a44ea3ac309163a4f59c476482c4907e8e0775f224045030abf10fa4f9e1cb7743db034429249b9e59354988e247eeb0f4fdcf languageName: node linkType: hard -"@radix-ui/react-collection@npm:1.0.3": - version: 1.0.3 - resolution: "@radix-ui/react-collection@npm:1.0.3" +"@radix-ui/react-collection@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-collection@npm:1.1.7" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-slot": "npm:1.0.2" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/2ac740ab746f411942dc95100f1eb60b9a3670960a805e266533fa1bc7dec31a6dabddd746ab788ebd5a9c22b468e38922f39d30447925515f8e44f0a3b2e56c + checksum: 10/cd53e2a2be82be7bc4014164cac0b42948401a203e5d0294d3947a5193f1d56bd23eb60e878a98dba50d08283254e79c3b873de5f935276b849686a868d51dd5 languageName: node linkType: hard -"@radix-ui/react-compose-refs@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-compose-refs@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" +"@radix-ui/react-compose-refs@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-compose-refs@npm:1.1.2" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8 + checksum: 10/9a91f0213014ffa40c5b8aae4debb993be5654217e504e35aa7422887eb2d114486d37e53c482d0fffb00cd44f51b5269fcdf397b280c71666fa11b7f32f165d languageName: node linkType: hard -"@radix-ui/react-context@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-context@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" +"@radix-ui/react-context@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-context@npm:1.1.2" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/a02187a3bae3a0f1be5fab5ad19c1ef06ceff1028d957e4d9994f0186f594a9c3d93ee34bacb86d1fa8eb274493362944398e1c17054d12cb3b75384f9ae564b + checksum: 10/156088367de42afa3c7e3acf5f0ba7cad6b359f3d17485585e80c2418434a6ed7cac2602eb73bca265d0091a1ad380f9405c069f103983e53497097ff35ba8f2 languageName: node linkType: hard "@radix-ui/react-dialog@npm:^1.0.4": - version: 1.0.5 - resolution: "@radix-ui/react-dialog@npm:1.0.5" + version: 1.1.15 + resolution: "@radix-ui/react-dialog@npm:1.1.15" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-dismissable-layer": "npm:1.0.5" - "@radix-ui/react-focus-guards": "npm:1.0.1" - "@radix-ui/react-focus-scope": "npm:1.0.4" - "@radix-ui/react-id": "npm:1.0.1" - "@radix-ui/react-portal": "npm:1.0.4" - "@radix-ui/react-presence": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-slot": "npm:1.0.2" - "@radix-ui/react-use-controllable-state": "npm:1.0.1" - aria-hidden: "npm:^1.1.1" - react-remove-scroll: "npm:2.5.5" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/adbd7301586db712616a0f8dd54a25e7544853cbf61b5d6e279215d479f57ac35157847ee424d54a7e707969a926ca0a7c28934400c9ac224bd0c7cc19229aca + checksum: 10/90ad9ea36d927a05bcc2701b471c2965f6d5d4f446511cd471e62235fc674186997dea081f52e18cb17a1e593828d94da3848e68864fa3acebe29df9b068b240 languageName: node linkType: hard -"@radix-ui/react-direction@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-direction@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" +"@radix-ui/react-direction@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-direction@npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/5336a8b0d4f1cde585d5c2b4448af7b3d948bb63a1aadb37c77771b0e5902dc6266e409cf35fd0edaca7f33e26424be19e64fb8f9d7f7be2d6f1714ea2764210 + checksum: 10/8cc330285f1d06829568042ca9aabd3295be4690ae93683033fc8632b5c4dfc60f5c1312f6e2cae27c196189c719de3cfbcf792ff74800f9ccae0ab4abc1bc92 languageName: node linkType: hard -"@radix-ui/react-dismissable-layer@npm:1.0.5": - version: 1.0.5 - resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5" +"@radix-ui/react-dismissable-layer@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" - "@radix-ui/react-use-escape-keydown": "npm:1.0.3" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-escape-keydown": "npm:1.1.1" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/f1626d69bb50ec226032bb7d8c5abaaf7359c2d7660309b0ed3daaedd91f30717573aac1a1cb82d589b7f915cf464b95a12da0a3b91b6acfefb6fbbc62b992de + checksum: 10/c20772588423379dee47fbe1d45c238c45a3bbe612eaf64a86576bf81821975e256d92ac71f9151e91b94a73068656143a11da9a3e77de7564d2a9926468e37a languageName: node linkType: hard "@radix-ui/react-dropdown-menu@npm:^2.0.5": - version: 2.0.6 - resolution: "@radix-ui/react-dropdown-menu@npm:2.0.6" + version: 2.1.16 + resolution: "@radix-ui/react-dropdown-menu@npm:2.1.16" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-id": "npm:1.0.1" - "@radix-ui/react-menu": "npm:2.0.6" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-use-controllable-state": "npm:1.0.1" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-menu": "npm:2.1.16" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/efa0728a25ea6689c6f31e02025528a21ca3bdc8a905c551ff356f3a66e024ef7fda62dc38564ac1310b211685357e37329616c72e371974d6bded4170ab43a2 + checksum: 10/da215196b5dde5619cdb424b1b5236159e4bb949974b7f4ffbf047d467c55116229a8f9cf07eae6457afefb4a2b07888bb30542f303045e05d90a4b072941ae2 languageName: node linkType: hard -"@radix-ui/react-focus-guards@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-focus-guards@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" - peerDependencies: - "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7 - languageName: node - linkType: hard - -"@radix-ui/react-focus-scope@npm:1.0.4": - version: 1.0.4 - resolution: "@radix-ui/react-focus-scope@npm:1.0.4" - dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" - peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: 10/3590e74c6b682737c7ac4bf8db41b3df7b09a0320f3836c619e487df9915451e5dafade9923a74383a7366c59e9436f5fff4301d70c0d15928e0e16b36e58bc9 - languageName: node - linkType: hard - -"@radix-ui/react-id@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-id@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-use-layout-effect": "npm:1.0.1" - peerDependencies: - "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/446a453d799cc790dd2a1583ff8328da88271bff64530b5a17c102fa7fb35eece3cf8985359d416f65e330cd81aa7b8fe984ea125fc4f4eaf4b3801d698e49fe - languageName: node - linkType: hard - -"@radix-ui/react-menu@npm:2.0.6": - version: 2.0.6 - resolution: "@radix-ui/react-menu@npm:2.0.6" - dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-collection": "npm:1.0.3" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-direction": "npm:1.0.1" - "@radix-ui/react-dismissable-layer": "npm:1.0.5" - "@radix-ui/react-focus-guards": "npm:1.0.1" - "@radix-ui/react-focus-scope": "npm:1.0.4" - "@radix-ui/react-id": "npm:1.0.1" - "@radix-ui/react-popper": "npm:1.1.3" - "@radix-ui/react-portal": "npm:1.0.4" - "@radix-ui/react-presence": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-roving-focus": "npm:1.0.4" - "@radix-ui/react-slot": "npm:1.0.2" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" - aria-hidden: "npm:^1.1.1" - react-remove-scroll: "npm:2.5.5" - peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: 10/8e8c41a46f4fab25b53c5400876f611372491e252d8ef763c3608e571df5aae5524c0a9c210780039b0de6d62affedaa16d189dd4c0148da0984f8f809311032 - languageName: node - linkType: hard - -"@radix-ui/react-popper@npm:1.1.3": +"@radix-ui/react-focus-guards@npm:1.1.3": version: 1.1.3 - resolution: "@radix-ui/react-popper@npm:1.1.3" + resolution: "@radix-ui/react-focus-guards@npm:1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b57878f6cf0ebc3e8d7c5c6bbaad44598daac19c921551ca541c104201048a9a902f3d69196e7a09995fd46e998c309aab64dc30fa184b3609d67d187a6a9c24 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-focus-scope@npm:1.1.7" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/2a7cd00e39e01756999ebf0bdb3401d6a8efa489a7b19e6b629b40bad3022b7b1f616555ccb4b0505bc0ba53e13a1fb51be905db138b16ec39c4fe319fe701d3 + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-id@npm:1.1.1" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/8d68e200778eb3038906870fc869b3d881f4a46715fb20cddd9c76cba42fdaaa4810a3365b6ec2daf0f185b9201fc99d009167f59c7921bc3a139722c2e976db + languageName: node + linkType: hard + +"@radix-ui/react-menu@npm:2.1.16": + version: 2.1.16 + resolution: "@radix-ui/react-menu@npm:2.1.16" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-roving-focus": "npm:1.1.11" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/2ffdfa08822c8c4ffc265d02d16c83d725114f9c0e9b510e73e431306dedddd507ef2861ccd67ec8c0d21cb24cd6401e42f16f3e65b30be627c7e22159151e40 + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-popper@npm:1.2.8" dependencies: - "@babel/runtime": "npm:^7.13.10" "@floating-ui/react-dom": "npm:^2.0.0" - "@radix-ui/react-arrow": "npm:1.0.3" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" - "@radix-ui/react-use-layout-effect": "npm:1.0.1" - "@radix-ui/react-use-rect": "npm:1.0.1" - "@radix-ui/react-use-size": "npm:1.0.1" - "@radix-ui/rect": "npm:1.0.1" + "@radix-ui/react-arrow": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + "@radix-ui/react-use-rect": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + "@radix-ui/rect": "npm:1.1.1" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/1f70ca09b609122058a58f57fa6bce7e528d96552c9db1a1d214e8e4a9dd305e473dfa0ac7dd400d3d215e54b5cf31020199aca3c2728dc1a716f4c7510838a5 + checksum: 10/01366054e1e63dd9394f77afb9da3367709478a5adf4436c080fc5bbe9456170192ff9d1425d9fae5b246e1ba95173848f84b6f2a06b21b47d966367ec7cb997 languageName: node linkType: hard -"@radix-ui/react-portal@npm:1.0.4": - version: 1.0.4 - resolution: "@radix-ui/react-portal@npm:1.0.4" +"@radix-ui/react-portal@npm:1.1.9": + version: 1.1.9 + resolution: "@radix-ui/react-portal@npm:1.1.9" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64 + checksum: 10/bd6be39bf021d5c917e2474ecba411e2625171f7ef96862b9af04bbd68833bb3662a7f1fbdeb5a7a237111b10e811e76d2cd03e957dadd6e668ef16541bfbd68 languageName: node linkType: hard -"@radix-ui/react-presence@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-presence@npm:1.0.1" +"@radix-ui/react-presence@npm:1.1.5": + version: 1.1.5 + resolution: "@radix-ui/react-presence@npm:1.1.5" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-use-layout-effect": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/406f0b5a54ea4e7881e15bddc3863234bb14bf3abd4a6e56ea57c6df6f9265a9ad5cfa158e3a98614f0dcbbb7c5f537e1f7158346e57cc3f29b522d62cf28823 + checksum: 10/4cdb05844c18877efb4b9739b46b7e5850b81d7ede994e75b5d62e8153a43c6e16b3ff9e55ff716e20b74b99b9415a94e97fd636bcb8698d5bbf7ab7b8663f9b languageName: node linkType: hard -"@radix-ui/react-primitive@npm:1.0.3": - version: 1.0.3 - resolution: "@radix-ui/react-primitive@npm:1.0.3" +"@radix-ui/react-primitive@npm:2.1.3": + version: 2.1.3 + resolution: "@radix-ui/react-primitive@npm:2.1.3" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-slot": "npm:1.0.2" + "@radix-ui/react-slot": "npm:1.2.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/bedb934ac07c710dc5550a7bfc7065d47e099d958cde1d37e4b1947ae5451f1b7e6f8ff5965e242578bf2c619065e6038c3a3aa779e5eafa7da3e3dbc685799f + checksum: 10/1dbbf932a3527f4e62f210bb72944eff605c3e38c8d3275ed5a5c570c02820ab156169756a65ad9a638d2089a828a04a7903795377384e98c87d0ca456303253 languageName: node linkType: hard -"@radix-ui/react-roving-focus@npm:1.0.4": - version: 1.0.4 - resolution: "@radix-ui/react-roving-focus@npm:1.0.4" +"@radix-ui/react-primitive@npm:2.1.4": + version: 2.1.4 + resolution: "@radix-ui/react-primitive@npm:2.1.4" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-collection": "npm:1.0.3" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-direction": "npm:1.0.1" - "@radix-ui/react-id": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" - "@radix-ui/react-use-controllable-state": "npm:1.0.1" + "@radix-ui/react-slot": "npm:1.2.4" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/a23ffb1e3e29a8209b94ce3857bf559dcf2175c4f316169dc47d018e8e94cd018dc914331a1d1762f32448e2594b7c8945efaa7059056f9940ce92cc35cc7026 + checksum: 10/a718537c2e66d541e72cc6b92bcf8ba6a7a0b4a30072efadeac0a517eb36f8acc55f2983cc1e345dc0eae18166ebcace4dab0299ed87a79e8ac3f653df9ee437 languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.0.2": - version: 1.0.2 - resolution: "@radix-ui/react-slot@npm:1.0.2" +"@radix-ui/react-roving-focus@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-roving-focus@npm:1.1.11" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-collection": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-direction": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/734866561e991438fbcf22af06e56b272ed6ee8f7b536489ee3bf2f736f8b53bf6bc14ebde94834aa0aceda854d018a0ce20bb171defffbaed1f566006cbb887 + "@types/react-dom": + optional: true + checksum: 10/0eddafa942332c95622ab8b53cce2fa25fd0dcaf4797218e9e6725da0734a81a438852cdcb3f588521018f68d38c6c5e50c64fda78c655f4e69dd45681ecc5e7 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-slot@npm:1.2.3" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/fe484c2741e31d9c20a8fb53c5790a73c0664e2bea35e27f4d484a90c42135fcfffe11a08abfcacb7a8ee2faf013471f0e856818f3ddac8ac51ceb8869e0fd08 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.2.4": + version: 1.2.4 + resolution: "@radix-ui/react-slot@npm:1.2.4" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b37e37455b92789758980359d73ab5a5f5d1c12af480c775519bd15c556b891642d472accf05b30d520751489ca74cdb8fd7866064abc7942f0437371be28e51 languageName: node linkType: hard "@radix-ui/react-tooltip@npm:^1.0.6": - version: 1.0.7 - resolution: "@radix-ui/react-tooltip@npm:1.0.7" + version: 1.2.8 + resolution: "@radix-ui/react-tooltip@npm:1.2.8" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/primitive": "npm:1.0.1" - "@radix-ui/react-compose-refs": "npm:1.0.1" - "@radix-ui/react-context": "npm:1.0.1" - "@radix-ui/react-dismissable-layer": "npm:1.0.5" - "@radix-ui/react-id": "npm:1.0.1" - "@radix-ui/react-popper": "npm:1.1.3" - "@radix-ui/react-portal": "npm:1.0.4" - "@radix-ui/react-presence": "npm:1.0.1" - "@radix-ui/react-primitive": "npm:1.0.3" - "@radix-ui/react-slot": "npm:1.0.2" - "@radix-ui/react-use-controllable-state": "npm:1.0.1" - "@radix-ui/react-visually-hidden": "npm:1.0.3" + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + "@radix-ui/react-visually-hidden": "npm:1.2.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/8f075a78db9bfe3dac251266feeb771923176d388c3232f9bad8d85417b5d80d2470697e1c7cae6765d3af16e48552ab9810137c2db193bc37e61b97388e92e8 + checksum: 10/e31857628d998b69616b8994f9627d387ed7bfa453b94e3b18ad2c04de83caf5fcca0ef2f304b1d343e00f183e937d883247d81e386dcc76c7c7c268484bc47c languageName: node linkType: hard -"@radix-ui/react-use-callback-ref@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" +"@radix-ui/react-use-callback-ref@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c + checksum: 10/cde8c40f1d4e79e6e71470218163a746858304bad03758ac84dc1f94247a046478e8e397518350c8d6609c84b7e78565441d7505bb3ed573afce82cfdcd19faf languageName: node linkType: hard -"@radix-ui/react-use-controllable-state@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" +"@radix-ui/react-use-controllable-state@npm:1.2.2": + version: 1.2.2 + resolution: "@radix-ui/react-use-controllable-state@npm:1.2.2" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-effect-event": "npm:0.0.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9 + checksum: 10/a100bff3ddecb753dab17444147273c9f70046c5949712c52174b259622eaef12acbf7ebcf289bae4e714eb84d0a7317c1aa44064cd997f327d77b62bc732a7c languageName: node linkType: hard -"@radix-ui/react-use-escape-keydown@npm:1.0.3": - version: 1.0.3 - resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" +"@radix-ui/react-use-effect-event@npm:0.0.2": + version: 0.0.2 + resolution: "@radix-ui/react-use-effect-event@npm:0.0.2" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/c6ed0d9ce780f67f924980eb305af1f6cce2a8acbaf043a58abe0aa3cc551d9aa76ccee14531df89bbee302ead7ecc7fce330886f82d4672c5eda52f357ef9b8 + checksum: 10/5a1950a30a399ea7e4b98154da9f536737a610de80189b7aacd4f064a89a3cd0d2a48571d527435227252e72e872bdb544ff6ffcfbdd02de2efd011be4aaa902 languageName: node linkType: hard -"@radix-ui/react-use-layout-effect@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" +"@radix-ui/react-use-escape-keydown@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.1" dependencies: - "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5 + checksum: 10/0eb0756c2c55ddcde9ff01446ab01c085ab2bf799173e97db7ef5f85126f9e8600225570801a1f64740e6d14c39ffe8eed7c14d29737345a5797f4622ac96f6f languageName: node linkType: hard -"@radix-ui/react-use-rect@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-use-rect@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/rect": "npm:1.0.1" +"@radix-ui/react-use-layout-effect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/433f07e61e04eb222349825bb05f3591fca131313a1d03709565d6226d8660bd1d0423635553f95ee4fcc25c8f2050972d848808d753c388e2a9ae191ebf17f3 + checksum: 10/bad2ba4f206e6255263582bedfb7868773c400836f9a1b423c0b464ffe4a17e13d3f306d1ce19cf7a19a492e9d0e49747464f2656451bb7c6a99f5a57bd34de2 languageName: node linkType: hard -"@radix-ui/react-use-size@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/react-use-size@npm:1.0.1" +"@radix-ui/react-use-rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-rect@npm:1.1.1" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-use-layout-effect": "npm:1.0.1" + "@radix-ui/rect": "npm:1.1.1" peerDependencies: "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/6cc150ad1e9fa85019c225c5a5d50a0af6cdc4653dad0c21b4b40cd2121f36ee076db326c43e6bc91a69766ccff5a84e917d27970176b592577deea3c85a3e26 + checksum: 10/116461bebc49472f7497e66a9bd413541181b3d00c5e0aaeef45d790dc1fbd7c8dcea80b169ea273306228b9a3c2b70067e902d1fd5004b3057e3bbe35b9d55d languageName: node linkType: hard -"@radix-ui/react-visually-hidden@npm:1.0.3, @radix-ui/react-visually-hidden@npm:^1.0.3": - version: 1.0.3 - resolution: "@radix-ui/react-visually-hidden@npm:1.0.3" +"@radix-ui/react-use-size@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-size@npm:1.1.1" dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/64e61f65feb67ffc80e1fc4a8d5e32480fb6d68475e2640377e021178dead101568cba5f936c9c33e6c142c7cf2fb5d76ad7b23ef80e556ba142d56cf306147b + languageName: node + linkType: hard + +"@radix-ui/react-visually-hidden@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.2.3" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" peerDependencies: "@types/react": "*" "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true "@types/react-dom": optional: true - checksum: 10/2e9d0c8253f97e7d6ffb2e52a5cfd40ba719f813b39c3e2e42c496d54408abd09ef66b5aec4af9b8ab0553215e32452a5d0934597a49c51dd90dc39181ed0d57 + checksum: 10/42296bde1ddf4af4e7445e914c35d6bc8406d6ede49f0a959a553e75b3ed21da09fda80a81c48d8ec058ed8129ce7137499d02ee26f90f0d3eaa2417922d6509 languageName: node linkType: hard -"@radix-ui/rect@npm:1.0.1": - version: 1.0.1 - resolution: "@radix-ui/rect@npm:1.0.1" +"@radix-ui/react-visually-hidden@npm:^1.0.3": + version: 1.2.4 + resolution: "@radix-ui/react-visually-hidden@npm:1.2.4" dependencies: - "@babel/runtime": "npm:^7.13.10" - checksum: 10/e25492cb8a683246161d781f0f3205f79507280a60f50eb763f06e8b6fa211b940b784aa581131ed76695bd5df5d1033a6246b43a6996cf8959a326fe4d3eb00 + "@radix-ui/react-primitive": "npm:2.1.4" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/43bbb68532748fabe8bec52628a7876714889f67e1a21d627c30816e53b68042ece849eef61fbd854af9c528b157cdd483ad032668c95d627ef27edd2530253f languageName: node linkType: hard -"@react-aria/autocomplete@npm:3.0.0-rc.4": - version: 3.0.0-rc.4 - resolution: "@react-aria/autocomplete@npm:3.0.0-rc.4" +"@radix-ui/rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/rect@npm:1.1.1" + checksum: 10/b6c5eb787640775b53dd52fa47218a089f0a0d8220d3ebff079c0b754e1fb82d89b6bdf08a82fd0d59549bdeb52678c0cca091c302da49dcf74c3c989cb55678 + languageName: node + linkType: hard + +"@react-aria/autocomplete@npm:3.0.0-rc.6": + version: 3.0.0-rc.6 + resolution: "@react-aria/autocomplete@npm:3.0.0-rc.6" dependencies: - "@react-aria/combobox": "npm:^3.14.1" - "@react-aria/focus": "npm:^3.21.3" - "@react-aria/i18n": "npm:^3.12.14" - "@react-aria/interactions": "npm:^3.26.0" - "@react-aria/listbox": "npm:^3.15.1" - "@react-aria/searchfield": "npm:^3.8.10" - "@react-aria/textfield": "npm:^3.18.3" - "@react-aria/utils": "npm:^3.32.0" + "@react-aria/combobox": "npm:^3.15.0" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/listbox": "npm:^3.15.3" + "@react-aria/searchfield": "npm:^3.8.12" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/utils": "npm:^3.33.1" "@react-stately/autocomplete": "npm:3.0.0-beta.4" - "@react-stately/combobox": "npm:^3.12.1" - "@react-types/autocomplete": "npm:3.0.0-alpha.36" - "@react-types/button": "npm:^3.14.1" - "@react-types/shared": "npm:^3.32.1" + "@react-stately/combobox": "npm:^3.13.0" + "@react-types/autocomplete": "npm:3.0.0-alpha.38" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3901ea60c43ff68b381a04db64577de07e46bc63e95c7cb122ace6053a0c54f58ba206704a2cd19799499f4a31ce6413ba65e8af39e675b5cedd66e9a5870729 + checksum: 10/5368f7597369d4b6054562a1812c6fce4ef9276a9618d1a7d043a89e9788415ed5a519ac1172edbaf4e458e5ca179ee6eca354f279515807eb7da8789c0ded90 languageName: node linkType: hard -"@react-aria/breadcrumbs@npm:^3.5.31": - version: 3.5.31 - resolution: "@react-aria/breadcrumbs@npm:3.5.31" +"@react-aria/breadcrumbs@npm:^3.5.32": + version: 3.5.32 + resolution: "@react-aria/breadcrumbs@npm:3.5.32" dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/link": "npm:^3.8.8" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/breadcrumbs": "npm:^3.7.18" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/link": "npm:^3.8.9" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/breadcrumbs": "npm:^3.7.19" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/80a1d6848e5230e34e4e4b0b75e395a2c4dfb78dbf4ae9f3e0bb8fe818568e92097df14f9b2d6502e4281a8cebb8da49dab040a49e1485be5281f38a8109ac64 + checksum: 10/37c6153bc330aea36ea55f891170c39f4f19be9b26ed366524e047e54503b7935118c12b57f8952e4e13d2a11835503f50b16952eec7e9477341c77d867b4d9d languageName: node linkType: hard -"@react-aria/button@npm:^3.14.3, @react-aria/button@npm:^3.14.4": - version: 3.14.4 - resolution: "@react-aria/button@npm:3.14.4" +"@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: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/toolbar": "npm:3.0.0-beta.23" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/toggle": "npm:^3.9.4" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/toolbar": "npm:3.0.0-beta.24" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/toggle": "npm:^3.9.5" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/1a02bc49d5190234e5a15d9a132ee19a3f9d3ca3178858df002e2d2e6a8f1435b14d0d8d2457fc1a3583987cd64917562ed7126717f18035bb8999540e31a34e + checksum: 10/944f33637f555b4c3d74c83969623f49fee65ddbd58c14badd04ebbff65a5cd09eb2ebf931ad275403ac824f122fd46a9b63804d33c27380747f7dd4d60c2666 languageName: node linkType: hard -"@react-aria/calendar@npm:^3.9.4": - version: 3.9.4 - resolution: "@react-aria/calendar@npm:3.9.4" +"@react-aria/calendar@npm:^3.9.5": + version: 3.9.5 + resolution: "@react-aria/calendar@npm:3.9.5" dependencies: - "@internationalized/date": "npm:^3.11.0" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" + "@internationalized/date": "npm:^3.12.0" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/calendar": "npm:^3.9.2" - "@react-types/button": "npm:^3.15.0" - "@react-types/calendar": "npm:^3.8.2" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/calendar": "npm:^3.9.3" + "@react-types/button": "npm:^3.15.1" + "@react-types/calendar": "npm:^3.8.3" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e26a987ee685df3132ec6ba368c9c84486bbe712bacb0286a71f824f33a86db454325421397bfb9c3b921de1e2d4d6f22e9d47d6a7c265a9f2c3c4868d332a36 + checksum: 10/358de2ab043c9369be3ca7738757c780c7c8612db7b38ae1d2af47fd16a1d8ec26f4db390df573ff5253d256e74e9163ad2a78a189ef5db60a5709d8b5f55aa1 languageName: node linkType: hard -"@react-aria/checkbox@npm:^3.16.4": - version: 3.16.4 - resolution: "@react-aria/checkbox@npm:3.16.4" +"@react-aria/checkbox@npm:^3.16.5": + version: 3.16.5 + resolution: "@react-aria/checkbox@npm:3.16.5" dependencies: - "@react-aria/form": "npm:^3.1.4" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/toggle": "npm:^3.12.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/checkbox": "npm:^3.7.4" - "@react-stately/form": "npm:^3.2.3" - "@react-stately/toggle": "npm:^3.9.4" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/form": "npm:^3.1.5" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/toggle": "npm:^3.12.5" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/checkbox": "npm:^3.7.5" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/toggle": "npm:^3.9.5" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c687f831f03f9fb2cf141d66a1090f19d70240c896eabc01ed5ef2b284b6a55f2be31d20ae582273c437bd5957f9436b991c91da1218f003d0190f16608d5168 + checksum: 10/3df98e59115545b323aee85e24a8cd19a812fb363c4641a7e7c3310280eb9d0e8c26b9c80b97f0ad3e5c8f7c84b9a320c38d05f78aaab5e3737c1aa01f19dd0a languageName: node linkType: hard -"@react-aria/collections@npm:^3.0.1": - version: 3.0.1 - resolution: "@react-aria/collections@npm:3.0.1" +"@react-aria/collections@npm:^3.0.3": + version: 3.0.3 + resolution: "@react-aria/collections@npm:3.0.3" dependencies: - "@react-aria/interactions": "npm:^3.26.0" + "@react-aria/interactions": "npm:^3.27.1" "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/utils": "npm:^3.32.0" - "@react-types/shared": "npm:^3.32.1" - "@swc/helpers": "npm:^0.5.0" - use-sync-external-store: "npm:^1.4.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e05897ccd0c994c2a257f6c9dbe167a4d7e5fbeae053833a746870072868ae3bc3f92c7e6f30e4c0d314b1f9450c8d1ffb67600ca89f6d5914577f5cc32ca363 - languageName: node - linkType: hard - -"@react-aria/color@npm:^3.1.4": - version: 3.1.4 - resolution: "@react-aria/color@npm:3.1.4" - dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/numberfield": "npm:^3.12.4" - "@react-aria/slider": "npm:^3.8.4" - "@react-aria/spinbutton": "npm:^3.7.1" - "@react-aria/textfield": "npm:^3.18.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-aria/visually-hidden": "npm:^3.8.30" - "@react-stately/color": "npm:^3.9.4" - "@react-stately/form": "npm:^3.2.3" - "@react-types/color": "npm:^3.1.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e5d4c8806f6963b334fc29fa74c2c9fe02af6b7123e64bc3d20e1a821cda11019e5673a4003b126156edf23314a3462cba05f9d393b4c25b1aa7b4392441e296 - languageName: node - linkType: hard - -"@react-aria/combobox@npm:^3.14.1, @react-aria/combobox@npm:^3.14.2": - version: 3.14.2 - resolution: "@react-aria/combobox@npm:3.14.2" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/listbox": "npm:^3.15.2" - "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/menu": "npm:^3.20.0" - "@react-aria/overlays": "npm:^3.31.1" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/textfield": "npm:^3.18.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/combobox": "npm:^3.12.2" - "@react-stately/form": "npm:^3.2.3" - "@react-types/button": "npm:^3.15.0" - "@react-types/combobox": "npm:^3.13.11" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c216da5a53ba570c3283cd0e9dd89364be44a706286566b6a2771a0a11cf4834cabca56388ed1e64c96c57c30371e95fef8b5146212b26fcfc7127df382493f6 - languageName: node - linkType: hard - -"@react-aria/datepicker@npm:^3.16.0": - version: 3.16.0 - resolution: "@react-aria/datepicker@npm:3.16.0" - dependencies: - "@internationalized/date": "npm:^3.11.0" - "@internationalized/number": "npm:^3.6.5" - "@internationalized/string": "npm:^3.2.7" - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/form": "npm:^3.1.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/spinbutton": "npm:^3.7.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/datepicker": "npm:^3.16.0" - "@react-stately/form": "npm:^3.2.3" - "@react-types/button": "npm:^3.15.0" - "@react-types/calendar": "npm:^3.8.2" - "@react-types/datepicker": "npm:^3.13.4" - "@react-types/dialog": "npm:^3.5.23" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/7fafd13c9d56c34c90f0b6ce875968752017d8206a30ba867e73a2a9067624d829273b12cd0242ae83d21ee6137d3007b30038abd7be5f1da4cefb5472ceb158 - languageName: node - linkType: hard - -"@react-aria/dialog@npm:^3.5.33": - version: 3.5.33 - resolution: "@react-aria/dialog@npm:3.5.33" - dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/overlays": "npm:^3.31.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/dialog": "npm:^3.5.23" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/ec30dcdaa93c5bb08bfa7e330fcb480aeed9933ce0a95009d934333e9eabe3d7e99d2600d62ee81acf833494ccfcb20ae51fa027d5d3efc944b948e9cc658117 - languageName: node - linkType: hard - -"@react-aria/disclosure@npm:^3.1.2": - version: 3.1.2 - resolution: "@react-aria/disclosure@npm:3.1.2" - dependencies: - "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/disclosure": "npm:^3.0.10" - "@react-types/button": "npm:^3.15.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/1beb8aed83bebe36ad76ac83e261b7f379babac28e94d297594b3a3fbde886b8cdbd18c1917d19bc7ad33f644db83a414b27b80ec2a9e2d62a5f4bf48694b1db - languageName: node - linkType: hard - -"@react-aria/dnd@npm:^3.11.4, @react-aria/dnd@npm:^3.11.5": - version: 3.11.5 - resolution: "@react-aria/dnd@npm:3.11.5" - dependencies: - "@internationalized/string": "npm:^3.2.7" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/overlays": "npm:^3.31.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/dnd": "npm:^3.7.3" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/906a9021028b2a5e58f8027bbbfb8744ff77b13e8721bfc365ecc241747503112234a7629edcc22d3dbedf4f28b183a59cc272000d9c7a3f315cba1320e0f429 - languageName: node - linkType: hard - -"@react-aria/focus@npm:^3.21.3, @react-aria/focus@npm:^3.21.4": - version: 3.21.4 - resolution: "@react-aria/focus@npm:3.21.4" - dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - clsx: "npm:^2.0.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/937b5234b7be34f656a919b77b70fbea815382165ffe61c7eda9c2f714385f8b525e3d7aca97fea23f3bb5390f047a080b86051f65bf11925375917c487aa556 - languageName: node - linkType: hard - -"@react-aria/form@npm:^3.1.4": - version: 3.1.4 - resolution: "@react-aria/form@npm:3.1.4" - dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/form": "npm:^3.2.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/ebf96186f559ec75a8ba3b44a1cdc230ec3ecb3fdf8140bb521a123aa920a8ec2144e5a6cf671cc91169d95b71265b75c6aeaf07b91e8e957c7ecd60ec0b89e3 - languageName: node - linkType: hard - -"@react-aria/grid@npm:^3.14.7": - version: 3.14.7 - resolution: "@react-aria/grid@npm:3.14.7" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/grid": "npm:^3.11.8" - "@react-stately/selection": "npm:^3.20.8" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/grid": "npm:^3.3.7" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/868e4eb5510854dadb9a008e8e5d7357df27ea69799b655661ea46ee8063cdbf5ee07c33b2fc6201faea5de2d1c04286055f563f688b7d27311a782feb55b371 - languageName: node - linkType: hard - -"@react-aria/gridlist@npm:^3.14.3": - version: 3.14.3 - resolution: "@react-aria/gridlist@npm:3.14.3" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/grid": "npm:^3.14.7" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/list": "npm:^3.13.3" - "@react-stately/tree": "npm:^3.9.5" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/932c9f25eef4a2b9767ec687333b73e74e955cb4c58d2e6ab634e7cc49aea2b1cd07a8e5763de28ff69421203cb65cb93ea760079a757fd5a56aef45b8251c69 - languageName: node - linkType: hard - -"@react-aria/i18n@npm:^3.12.14, @react-aria/i18n@npm:^3.12.15": - version: 3.12.15 - resolution: "@react-aria/i18n@npm:3.12.15" - dependencies: - "@internationalized/date": "npm:^3.11.0" - "@internationalized/message": "npm:^3.1.8" - "@internationalized/number": "npm:^3.6.5" - "@internationalized/string": "npm:^3.2.7" - "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/5e7432fc27d1dd1fa2093bff421d6c531665dade4cd5ab6535bdc0bca96e3274d6b2bcf93689e7f780ea1e7981f9a2fabd24da65dabf1d87b3d3a86b1e74a2b2 - languageName: node - linkType: hard - -"@react-aria/interactions@npm:^3.26.0, @react-aria/interactions@npm:^3.27.0": - version: 3.27.0 - resolution: "@react-aria/interactions@npm:3.27.0" - dependencies: - "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/flags": "npm:^3.1.2" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/97628ef923bd1ac9e549919e46713cd0c5384aff869db74c3920ecb964415c6b5a1d1f1d76b93c26f542243ff469c884c046de1bc831dde51f1d4ccf4c634ded - languageName: node - linkType: hard - -"@react-aria/label@npm:^3.7.24": - version: 3.7.24 - resolution: "@react-aria/label@npm:3.7.24" - dependencies: - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/ea96036b21537e850eb7d3cd1b17d270300b0f94220a1f41fe84be876c6e0c2ebb80addd7862fde15fbd71412dd2ffe6ecc845a9a327eb70edd3914c1987d305 - languageName: node - linkType: hard - -"@react-aria/landmark@npm:^3.0.9": - version: 3.0.9 - resolution: "@react-aria/landmark@npm:3.0.9" - dependencies: - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" use-sync-external-store: "npm:^1.6.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/a72402887ea066ba4b21e3d91b21c07b523562cb50085461685e4995c16822c55d6fab538c768c90608f14d68d66ebcd40025d3f959d79e2dc38f105e76fee8a + checksum: 10/099da897b6af195aef54f214facc16cadf524c2c194fd80eab795ae22789277a5870e961ac3307b4c9ab7595f64aa13b8e834c2e545ef6aed4817101361bff50 languageName: node linkType: hard -"@react-aria/link@npm:^3.8.8": - version: 3.8.8 - resolution: "@react-aria/link@npm:3.8.8" +"@react-aria/color@npm:^3.1.5": + version: 3.1.5 + resolution: "@react-aria/color@npm:3.1.5" dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/link": "npm:^3.6.6" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/numberfield": "npm:^3.12.5" + "@react-aria/slider": "npm:^3.8.5" + "@react-aria/spinbutton": "npm:^3.7.2" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/visually-hidden": "npm:^3.8.31" + "@react-stately/color": "npm:^3.9.5" + "@react-stately/form": "npm:^3.2.4" + "@react-types/color": "npm:^3.1.4" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/5300ad7d66f7b11b2986d8411453e57aeb473b0b7ccf5bc80e8cf54da2eff098b29313f4f364d6d577de7d3f04d87effb5955328f5413b5c99d64c95c5d950da + checksum: 10/7a1d96fdc5e87c9d9eea364f57d580d739fafcb71c6c97b4c558088376b5aa3d7e6b0000da8fb6a42aa28bcfafd85afb62247c51c716ba33a71d741747bfa29b languageName: node linkType: hard -"@react-aria/listbox@npm:^3.15.1, @react-aria/listbox@npm:^3.15.2": - version: 3.15.2 - resolution: "@react-aria/listbox@npm:3.15.2" +"@react-aria/combobox@npm:^3.15.0": + version: 3.15.0 + resolution: "@react-aria/combobox@npm:3.15.0" dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/list": "npm:^3.13.3" - "@react-types/listbox": "npm:^3.7.5" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/listbox": "npm:^3.15.3" + "@react-aria/live-announcer": "npm:^3.4.4" + "@react-aria/menu": "npm:^3.21.0" + "@react-aria/overlays": "npm:^3.31.2" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/combobox": "npm:^3.13.0" + "@react-stately/form": "npm:^3.2.4" + "@react-types/button": "npm:^3.15.1" + "@react-types/combobox": "npm:^3.14.0" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/b39f2c97f0831b069c721715ef5d403a9024abeeb018e26a05363dfda338afb978957dee581d6424a10fd2cc54105e3d0eb448642d0f95979fb912edb5a82c46 + checksum: 10/aca358e7be417159787b9b598754e94fbb88280eff6401f65facc48cb62da8485d3b0652bcf329f01e0081d20a4925b94f318dab7fed34341f38bb09b9bf7202 + languageName: node + linkType: hard + +"@react-aria/datepicker@npm:^3.16.1": + version: 3.16.1 + resolution: "@react-aria/datepicker@npm:3.16.1" + dependencies: + "@internationalized/date": "npm:^3.12.0" + "@internationalized/number": "npm:^3.6.5" + "@internationalized/string": "npm:^3.2.7" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/form": "npm:^3.1.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/spinbutton": "npm:^3.7.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/datepicker": "npm:^3.16.1" + "@react-stately/form": "npm:^3.2.4" + "@react-types/button": "npm:^3.15.1" + "@react-types/calendar": "npm:^3.8.3" + "@react-types/datepicker": "npm:^3.13.5" + "@react-types/dialog": "npm:^3.5.24" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/c8a9e5d0fc48c51c86e720007e0be835e33f722e4e15248a3e7232c6a78148784cab85f18fdd5ccfc1c7632d1c89e6602c5118abecbbab0097577c6daaf95bb2 + languageName: node + linkType: hard + +"@react-aria/dialog@npm:^3.5.34": + version: 3.5.34 + resolution: "@react-aria/dialog@npm:3.5.34" + dependencies: + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/overlays": "npm:^3.31.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/dialog": "npm:^3.5.24" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/9857bf323175341425a0f79952363c62aaebbe6271a3fa3ae9b4b34fb1190e072dd08ffd21ef13a722b6add079142d4728acd112c7cc6d0005ac00d3bb4c9cb1 + languageName: node + linkType: hard + +"@react-aria/disclosure@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-aria/disclosure@npm:3.1.3" + dependencies: + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/disclosure": "npm:^3.0.11" + "@react-types/button": "npm:^3.15.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/cc64f4131d8985f48ce2becacd536a74ea4f291e157409414119389fe5ec9034ade2332f27efed09e3b50ac04bbe1a7a2a707ca355e7daef3e59349ba56f41b3 + languageName: node + linkType: hard + +"@react-aria/dnd@npm:^3.11.6": + version: 3.11.6 + resolution: "@react-aria/dnd@npm:3.11.6" + dependencies: + "@internationalized/string": "npm:^3.2.7" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/live-announcer": "npm:^3.4.4" + "@react-aria/overlays": "npm:^3.31.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/dnd": "npm:^3.7.4" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/1d11e7de438154d72ad6acd394919bbc4e82be3beba9d552e7b59590f6a90f671e146d10cad9db1bcb5db9991b05c0c04bbce991d4e24508548079005f2d5920 + languageName: node + linkType: hard + +"@react-aria/focus@npm:^3.21.5": + version: 3.21.5 + resolution: "@react-aria/focus@npm:3.21.5" + dependencies: + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + clsx: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/cfc2698626a221b978213104d7d5354e2ab24467397e4c74879a0cb23733a004ca19c40fa55de0927b7d3d9156562b00c4e1e84e20f4894afd706cd8a31400e5 + languageName: node + linkType: hard + +"@react-aria/form@npm:^3.1.5": + version: 3.1.5 + resolution: "@react-aria/form@npm:3.1.5" + dependencies: + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/form": "npm:^3.2.4" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/696f6ae4325cf3cfdc8d8fff6a4f614b3a96345ff1c1d0a96875ba8c82c70954acf174ed6d44d397026b598b567bdd75ba84a0ce400b7eb30963cc45df2f2f56 + languageName: node + linkType: hard + +"@react-aria/grid@npm:^3.14.8": + version: 3.14.8 + resolution: "@react-aria/grid@npm:3.14.8" + dependencies: + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/live-announcer": "npm:^3.4.4" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/grid": "npm:^3.11.9" + "@react-stately/selection": "npm:^3.20.9" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/7a7bf816b48a441661628aaf3f439b1a7f04fc4719f7c52c7ee6f1530d60df447c6b74a7cfe740bb3b8e344a0931f22092be9496c90c1a24bedb05900cce958d + languageName: node + linkType: hard + +"@react-aria/gridlist@npm:^3.14.4": + version: 3.14.4 + resolution: "@react-aria/gridlist@npm:3.14.4" + dependencies: + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/grid": "npm:^3.14.8" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/list": "npm:^3.13.4" + "@react-stately/tree": "npm:^3.9.6" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/94d25960ee8f1e6d5325ddced6e15b3be93461ab454bc20aa5267ffcc086d5bfbf176aeaa53f8ad29ffc406dcbab4a02ac393439cc3cd93a8c243fddb4629809 + languageName: node + linkType: hard + +"@react-aria/i18n@npm:^3.12.16": + version: 3.12.16 + resolution: "@react-aria/i18n@npm:3.12.16" + dependencies: + "@internationalized/date": "npm:^3.12.0" + "@internationalized/message": "npm:^3.1.8" + "@internationalized/number": "npm:^3.6.5" + "@internationalized/string": "npm:^3.2.7" + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/45cf923204c97cab9baa98dbfa2e4ea1c72218939af93992affe3b4b733125533ed7e86d2ed5410b86eb299f2e9afddcc97907fe8523d7027cc5aee1796e7c8d + languageName: node + linkType: hard + +"@react-aria/interactions@npm:^3.27.1": + version: 3.27.1 + resolution: "@react-aria/interactions@npm:3.27.1" + dependencies: + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/flags": "npm:^3.1.2" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/58ef760bfbede659991d670516ac2c04b6230461ae963370d3fa2a86d9da2f8a27c17f76a6549c95af6845fa40f2c3e23d7574748347b6c3bc9581ca36bbaacd + languageName: node + linkType: hard + +"@react-aria/label@npm:^3.7.25": + version: 3.7.25 + resolution: "@react-aria/label@npm:3.7.25" + dependencies: + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a20fc05c5d16a7086b0d3979608f5f17ff3b830e1ca1760362e1ae8f54842ae4f2d3222c13c00d49aca6f99620b218640dc01dc2717d7d9bd3e9c4156cbfbdcc + languageName: node + linkType: hard + +"@react-aria/landmark@npm:^3.0.10": + version: 3.0.10 + resolution: "@react-aria/landmark@npm:3.0.10" + dependencies: + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/958a3a4da6968f5cb78b07e78beb1572388d2c5e9d9891080fbabfe482d7bdfba54653a85e4256ec1f4519d1bed02133562c2c777a00621cd6a978c7987a18b8 + languageName: node + linkType: hard + +"@react-aria/link@npm:^3.8.9": + version: 3.8.9 + resolution: "@react-aria/link@npm:3.8.9" + dependencies: + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/link": "npm:^3.6.7" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/46fd9abd934823761ec82afa63d70358443ad67343b930e1cd9482f1149b4fb62ea54968c7cc2cced63453d973116ebfdb84111a7708f95ccb312fc54ac84596 + languageName: node + linkType: hard + +"@react-aria/listbox@npm:^3.15.3": + version: 3.15.3 + resolution: "@react-aria/listbox@npm:3.15.3" + dependencies: + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/list": "npm:^3.13.4" + "@react-types/listbox": "npm:^3.7.6" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/9e1037c98d17025ad1164d170081527d8486d986092fb550367dfc92efdc226a85da55ad744e42fea744c86cea5d0f09e081765e53551ba1f212bfc3db936f78 languageName: node linkType: hard @@ -15963,237 +16084,239 @@ __metadata: languageName: node linkType: hard -"@react-aria/menu@npm:^3.20.0": - version: 3.20.0 - resolution: "@react-aria/menu@npm:3.20.0" +"@react-aria/menu@npm:^3.21.0": + version: 3.21.0 + resolution: "@react-aria/menu@npm:3.21.0" dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/overlays": "npm:^3.31.1" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/menu": "npm:^3.9.10" - "@react-stately/selection": "npm:^3.20.8" - "@react-stately/tree": "npm:^3.9.5" - "@react-types/button": "npm:^3.15.0" - "@react-types/menu": "npm:^3.10.6" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/overlays": "npm:^3.31.2" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/menu": "npm:^3.9.11" + "@react-stately/selection": "npm:^3.20.9" + "@react-stately/tree": "npm:^3.9.6" + "@react-types/button": "npm:^3.15.1" + "@react-types/menu": "npm:^3.10.7" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e405328801a2a0f7d77dc8a7b1a1f734305fc155cdef11be9cb1f7fd5c50f383536749587f8eff7f09fa71105a6e38467222a1ac7853cefec2aff57fa6f93b72 + checksum: 10/b41e5b19ab3c637f19ea387fd861bfaf868f7619325434abeacc3a047af80ce5a90ae604c60656d2586ad76335cdf830ee8e327d1ed3bdb730e50477c08c420c languageName: node linkType: hard -"@react-aria/meter@npm:^3.4.29": - version: 3.4.29 - resolution: "@react-aria/meter@npm:3.4.29" +"@react-aria/meter@npm:^3.4.30": + version: 3.4.30 + resolution: "@react-aria/meter@npm:3.4.30" dependencies: - "@react-aria/progress": "npm:^3.4.29" - "@react-types/meter": "npm:^3.4.14" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/progress": "npm:^3.4.30" + "@react-types/meter": "npm:^3.4.15" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/0ba421d54a86c9bea5317f27d7104bfdee9817f9c74251fcffa5ae277c2d3596441f2e146a7df58ff3ceb3711ecd2a2b37b5a379c05df2cd93c148de14a9df4d + checksum: 10/8d0f274aa3ca9da0cff9ca5ad65444978dd6edea51cf98a5560406e33ec614ebfe73cb1d7738d7cad7c62cf7e39ade0f38fbd65f63b2f7c68d2d8008f316578f languageName: node linkType: hard -"@react-aria/numberfield@npm:^3.12.4": - version: 3.12.4 - resolution: "@react-aria/numberfield@npm:3.12.4" +"@react-aria/numberfield@npm:^3.12.5": + version: 3.12.5 + resolution: "@react-aria/numberfield@npm:3.12.5" dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/spinbutton": "npm:^3.7.1" - "@react-aria/textfield": "npm:^3.18.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/form": "npm:^3.2.3" - "@react-stately/numberfield": "npm:^3.10.4" - "@react-types/button": "npm:^3.15.0" - "@react-types/numberfield": "npm:^3.8.17" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/bf0a0507f6e78420a720de10fae896a2be0d08944ee30e8a24881c6f93e402573bcc6bd0f1c45d0e070c44f92fe9ca7985e36f66b9aee06744a3d7d33f254fe2 - languageName: node - linkType: hard - -"@react-aria/overlays@npm:^3.31.0, @react-aria/overlays@npm:^3.31.1": - version: 3.31.1 - resolution: "@react-aria/overlays@npm:3.31.1" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/utils": "npm:^3.33.0" - "@react-aria/visually-hidden": "npm:^3.8.30" - "@react-stately/overlays": "npm:^3.6.22" - "@react-types/button": "npm:^3.15.0" - "@react-types/overlays": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/ebbd35a856bc78bf00b25af92291d2d55c6285478d34c9fe668f7164e6ecfb6c00154f373f60bcc959f8662a352a345733813fda89c09440f086e64f57835423 - languageName: node - linkType: hard - -"@react-aria/progress@npm:^3.4.29": - version: 3.4.29 - resolution: "@react-aria/progress@npm:3.4.29" - dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/progress": "npm:^3.5.17" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e042e35f550bdb94d3e9a0220200e6ac454b12ada425173455c9350f45867e31459422ef1298eb8a20b5ed67e850c50a3c72fb6b63f55cf7e3e0613f899f799d - languageName: node - linkType: hard - -"@react-aria/radio@npm:^3.12.4": - version: 3.12.4 - resolution: "@react-aria/radio@npm:3.12.4" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/form": "npm:^3.1.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/radio": "npm:^3.11.4" - "@react-types/radio": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/988a8d3be527ac7132be753c48807dd4ce644e6c7ecbbcf855c3df90e4901ea63511e8554bfef1e5cc1d3bdacec30b2c2962aaaff613b9c49661e496629aac9e - languageName: node - linkType: hard - -"@react-aria/searchfield@npm:^3.8.10, @react-aria/searchfield@npm:^3.8.11": - version: 3.8.11 - resolution: "@react-aria/searchfield@npm:3.8.11" - dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/textfield": "npm:^3.18.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/searchfield": "npm:^3.5.18" - "@react-types/button": "npm:^3.15.0" - "@react-types/searchfield": "npm:^3.6.7" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c504e4c81d00359a97844d692b7cfacfed220a4debad7d6885ac22224aac29a5d2899300b17bf38dc5f07a0eb71575000343457b182ab45839ba51b1ae1fc0ed - languageName: node - linkType: hard - -"@react-aria/select@npm:^3.17.2": - version: 3.17.2 - resolution: "@react-aria/select@npm:3.17.2" - dependencies: - "@react-aria/form": "npm:^3.1.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/listbox": "npm:^3.15.2" - "@react-aria/menu": "npm:^3.20.0" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-aria/visually-hidden": "npm:^3.8.30" - "@react-stately/select": "npm:^3.9.1" - "@react-types/button": "npm:^3.15.0" - "@react-types/select": "npm:^3.12.1" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/f77ef379d7e082b2350da59b82fae7ba01c7d2af12f36ee6926e3c75342a357d036006257aff5f76f6f37f2c296bd47b84c6ebce0ac66d730f8cae0827905337 - languageName: node - linkType: hard - -"@react-aria/selection@npm:^3.27.1": - version: 3.27.1 - resolution: "@react-aria/selection@npm:3.27.1" - dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/selection": "npm:^3.20.8" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3509f3afdc95ae733e70754e9638bc0e108c6d79eda3409e0b0ed154f441a0c1d3edf9d6095357d5c8a03bd7d27137f05c6022a094a0ca5890a76fe862e5e2ca - languageName: node - linkType: hard - -"@react-aria/separator@npm:^3.4.15": - version: 3.4.15 - resolution: "@react-aria/separator@npm:3.4.15" - dependencies: - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3a806efed72c77998befc6957ed0aac0c79ab504eb74b6c152a82e449b0285bf5cf4afc7bd9311b90dcab4e50fc6b8c0b93acc061b9db4d803db938ba3acc95e - languageName: node - linkType: hard - -"@react-aria/slider@npm:^3.8.4": - version: 3.8.4 - resolution: "@react-aria/slider@npm:3.8.4" - dependencies: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/slider": "npm:^3.7.4" - "@react-types/shared": "npm:^3.33.0" - "@react-types/slider": "npm:^3.8.3" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/46d88c01632011d4be9e9cb0b6ca8eb0a79d254f15f49823f195800ab38bed02f1a3e3ce758d2b7d8d1c2026f0f1948be164a156c5e72e4fd9d8f401d69db4bc - languageName: node - linkType: hard - -"@react-aria/spinbutton@npm:^3.7.1": - version: 3.7.1 - resolution: "@react-aria/spinbutton@npm:3.7.1" - dependencies: - "@react-aria/i18n": "npm:^3.12.15" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/spinbutton": "npm:^3.7.2" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/numberfield": "npm:^3.11.0" + "@react-types/button": "npm:^3.15.1" + "@react-types/numberfield": "npm:^3.8.18" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/29c9d01c3ae88af41b7fc36429d2230ffd5254cca48e7afce3032b4ffe529db6bdc94ba85b37cd97f6b39017aa1561ef56ffe26901970b836d91243de6a8f07d + checksum: 10/c0dd7690c854573be0abb0e9492297dccf0eb2cd09759281a9ce23825853c05ed6fd46ba71bce1a8fa814bca1bb28060c2de19abaeca5a2d7a3000352d7975ea + languageName: node + linkType: hard + +"@react-aria/overlays@npm:^3.31.2": + version: 3.31.2 + resolution: "@react-aria/overlays@npm:3.31.2" + dependencies: + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/ssr": "npm:^3.9.10" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/visually-hidden": "npm:^3.8.31" + "@react-stately/flags": "npm:^3.1.2" + "@react-stately/overlays": "npm:^3.6.23" + "@react-types/button": "npm:^3.15.1" + "@react-types/overlays": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/fdd732da9c5d1f4848a59052978992ff4a0dffb24567af6b688bdc0497fa21f6e5978d6d36e3607741685cf542ee70a9c60fa0775704cd3b1627726cf9468cb9 + languageName: node + linkType: hard + +"@react-aria/progress@npm:^3.4.30": + version: 3.4.30 + resolution: "@react-aria/progress@npm:3.4.30" + dependencies: + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/progress": "npm:^3.5.18" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/f463723e7a252110980e47fc5258e8e60f6ec503351ff06cb633dd46a34fa5b16e806441ba2db6af2590afb55ba69473b3e94b0f3d2c602a3d5a53c9a1e13eec + languageName: node + linkType: hard + +"@react-aria/radio@npm:^3.12.5": + version: 3.12.5 + resolution: "@react-aria/radio@npm:3.12.5" + dependencies: + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/form": "npm:^3.1.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/radio": "npm:^3.11.5" + "@react-types/radio": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/9e88093d9eba0fef7ecb7ebc007745b3af9c7f6aa722269233c5b3bdd84d699c4866529f488ac46f08386e8515b3dad717b37faf41cbd9954840a7aa7b5056ac + languageName: node + linkType: hard + +"@react-aria/searchfield@npm:^3.8.12": + version: 3.8.12 + resolution: "@react-aria/searchfield@npm:3.8.12" + dependencies: + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/searchfield": "npm:^3.5.19" + "@react-types/button": "npm:^3.15.1" + "@react-types/searchfield": "npm:^3.6.8" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/12c3bd063b361ea2a9c4b2f4a1b6888b9bd98d419ccdf3684f32f0cafb5a99a19cfe858f002a49f5a969cbf02756656144b0d2d87d46c547b31d64770cfe6fdc + languageName: node + linkType: hard + +"@react-aria/select@npm:^3.17.3": + version: 3.17.3 + resolution: "@react-aria/select@npm:3.17.3" + dependencies: + "@react-aria/form": "npm:^3.1.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/listbox": "npm:^3.15.3" + "@react-aria/menu": "npm:^3.21.0" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/visually-hidden": "npm:^3.8.31" + "@react-stately/select": "npm:^3.9.2" + "@react-types/button": "npm:^3.15.1" + "@react-types/select": "npm:^3.12.2" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/0741f0cc13073ae80e1ac3241bc40c54588fc6da4eb74360becb585d723b021b7f9cea7b6365bb22a307e8539b307fbc2c0b31a304118f10eda0a98f503a303a + languageName: node + linkType: hard + +"@react-aria/selection@npm:^3.27.2": + version: 3.27.2 + resolution: "@react-aria/selection@npm:3.27.2" + dependencies: + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/selection": "npm:^3.20.9" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/b6730dad9f3f532780a1ccd8fab2e1777dab71d8f38e2bbdb94903df4fe8cde81e843a026d8d24a0fe9b4899f1dccdddc29734ad57e4a758618d3a5226ef2dd2 + languageName: node + linkType: hard + +"@react-aria/separator@npm:^3.4.16": + version: 3.4.16 + resolution: "@react-aria/separator@npm:3.4.16" + dependencies: + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/697775caf657493a469c445cde847bbe800fe6b199ef852a6fce6b9e78775b126ee76089ebd86d3defe640506d2c218dc34a737745cbc2ee4175aa87ad06be82 + languageName: node + linkType: hard + +"@react-aria/slider@npm:^3.8.5": + version: 3.8.5 + resolution: "@react-aria/slider@npm:3.8.5" + dependencies: + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/slider": "npm:^3.7.5" + "@react-types/shared": "npm:^3.33.1" + "@react-types/slider": "npm:^3.8.4" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/d832288bf797b4815ad1611f2117a3ccac8c514daf4801cefb8e3e6a3f9683ac038d795506870641f9047448d4f7dc07b67bb5a0c65b779d3b86266921492a81 + languageName: node + linkType: hard + +"@react-aria/spinbutton@npm:^3.7.2": + version: 3.7.2 + resolution: "@react-aria/spinbutton@npm:3.7.2" + dependencies: + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/live-announcer": "npm:^3.4.4" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/ccf48a64cbc6942d35eebc79c1559baa9eccae6a4d1c4cc6026d9d6b6aa09a5eeb8de75126173c4c457647bb43867f10bc9548bc151769f75ab461d0e34aeb34 languageName: node linkType: hard @@ -16208,258 +16331,242 @@ __metadata: languageName: node linkType: hard -"@react-aria/switch@npm:^3.7.10": - version: 3.7.10 - resolution: "@react-aria/switch@npm:3.7.10" +"@react-aria/switch@npm:^3.7.11": + version: 3.7.11 + resolution: "@react-aria/switch@npm:3.7.11" dependencies: - "@react-aria/toggle": "npm:^3.12.4" - "@react-stately/toggle": "npm:^3.9.4" - "@react-types/shared": "npm:^3.33.0" - "@react-types/switch": "npm:^3.5.16" + "@react-aria/toggle": "npm:^3.12.5" + "@react-stately/toggle": "npm:^3.9.5" + "@react-types/shared": "npm:^3.33.1" + "@react-types/switch": "npm:^3.5.17" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/8567f753bba85a4d97be02730caeed314470867bfda86921a08b357b8e1b9c2b74d6feced26a2dd017d7dfe0bbe684512dedc736f946ab2ba3ebc17c04cf65c6 + checksum: 10/d4a3ad78cfe264b7f595287c3b516f9d46d38523d6e5b1562a4f86ebe2c099bbca7203ccdf395634484f278dd69f9821cdc1c5a80f6d1dc7c7a73b27f507939a languageName: node linkType: hard -"@react-aria/table@npm:^3.17.10": - version: 3.17.10 - resolution: "@react-aria/table@npm:3.17.10" +"@react-aria/table@npm:^3.17.11": + version: 3.17.11 + resolution: "@react-aria/table@npm:3.17.11" dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/grid": "npm:^3.14.7" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/grid": "npm:^3.14.8" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/utils": "npm:^3.33.0" - "@react-aria/visually-hidden": "npm:^3.8.30" - "@react-stately/collections": "npm:^3.12.9" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/visually-hidden": "npm:^3.8.31" + "@react-stately/collections": "npm:^3.12.10" "@react-stately/flags": "npm:^3.1.2" - "@react-stately/table": "npm:^3.15.3" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/grid": "npm:^3.3.7" - "@react-types/shared": "npm:^3.33.0" - "@react-types/table": "npm:^3.13.5" + "@react-stately/table": "npm:^3.15.4" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + "@react-types/table": "npm:^3.13.6" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/87415c6f5cdaac7710065b630555c31edcc5300b680a0e93d663aa30018eb90070f133dae2b94c07969cd63a069317120f480bce1add2d69b2d511e692dadb4d + checksum: 10/66747f768cacf954e285a4b823c971f27d07f58fff06de65ee9c5cd8f10ba2acbb1675f3b8757ceece5c6bb2997694e0d2b1e7ebb9e7e06f61d8e3f491c72589 languageName: node linkType: hard -"@react-aria/tabs@npm:^3.11.0": - version: 3.11.0 - resolution: "@react-aria/tabs@npm:3.11.0" +"@react-aria/tabs@npm:^3.11.1": + version: 3.11.1 + resolution: "@react-aria/tabs@npm:3.11.1" dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/tabs": "npm:^3.8.8" - "@react-types/shared": "npm:^3.33.0" - "@react-types/tabs": "npm:^3.3.21" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/tabs": "npm:^3.8.9" + "@react-types/shared": "npm:^3.33.1" + "@react-types/tabs": "npm:^3.3.22" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/137480e2b4dbb7d8d52b22927fc455d76555e1584eee945fd64380c0beb6d50b4d37a85db099fa0dd2bf92cb39cad653d826cc034aacdbe1e5e660f333f40f49 + checksum: 10/dc8d9ff2f316fadc6bdde2447f1eb9f013fcf97d18c21e61572de1a304da6fb2794594ccb3de2f3237e0094ef78f8df19d01c07c10ed0723116e90fe00e2cd73 languageName: node linkType: hard -"@react-aria/tag@npm:^3.8.0": - version: 3.8.0 - resolution: "@react-aria/tag@npm:3.8.0" +"@react-aria/tag@npm:^3.8.1": + version: 3.8.1 + resolution: "@react-aria/tag@npm:3.8.1" dependencies: - "@react-aria/gridlist": "npm:^3.14.3" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/list": "npm:^3.13.3" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/gridlist": "npm:^3.14.4" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/list": "npm:^3.13.4" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/6e94c3b38bf9b5771ac1bdec1b74935b7b6d6c24794a17aa242354412896688e22e61945985f93d8a46b7866bfa1e36264b391dae4ee3a49b8b579bdc37751cc + checksum: 10/246529ed55848886a630274ae1a1ce40807653d7d4215ff58472b7d5b6ed2a26c76413ab45c88db023a07ab3c2407edf24b9a525fd557b06f80ae689a32dc9ba languageName: node linkType: hard -"@react-aria/textfield@npm:^3.18.3, @react-aria/textfield@npm:^3.18.4": - version: 3.18.4 - resolution: "@react-aria/textfield@npm:3.18.4" +"@react-aria/textfield@npm:^3.18.5": + version: 3.18.5 + resolution: "@react-aria/textfield@npm:3.18.5" dependencies: - "@react-aria/form": "npm:^3.1.4" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/form": "npm:^3.2.3" + "@react-aria/form": "npm:^3.1.5" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/form": "npm:^3.2.4" "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" - "@react-types/textfield": "npm:^3.12.7" + "@react-types/shared": "npm:^3.33.1" + "@react-types/textfield": "npm:^3.12.8" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/01187b9f62422db134b13c0cdaf015e60194150e6c99dea7b0a56996d14bad607cfb5c37ca2b9760128ac128392f4c123a2adba2ba2f70474ddc4e1163a834fe + checksum: 10/fbad840d923ad9c63462995b9fd7a845775e8e67b8624cee26223a09252f420f5cc3e307e6f08a338fb057dbcc3b18bc6cf4fb1337e27f193efe5ce454beb3bf languageName: node linkType: hard -"@react-aria/toast@npm:^3.0.10, @react-aria/toast@npm:^3.0.9": - version: 3.0.10 - resolution: "@react-aria/toast@npm:3.0.10" +"@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: - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/landmark": "npm:^3.0.9" - "@react-aria/utils": "npm:^3.33.0" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/landmark": "npm:^3.0.10" + "@react-aria/utils": "npm:^3.33.1" "@react-stately/toast": "npm:^3.1.3" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/59d3b42ad7569fec345bf835cca2326613539bd18316183f933259475c40d595a87b988373aff9920b2f60e8dca8002022b430f1561d235d8d723bd6828f3d11 + checksum: 10/06369d0b65e4c1309914e07e73584d60743dde0425f276fb531ac85f3a5ecc3dbde295f759a64167af3aec624afd472ac17dd8516e74354ec2d875d21f2e71e4 languageName: node linkType: hard -"@react-aria/toggle@npm:^3.12.4": - version: 3.12.4 - resolution: "@react-aria/toggle@npm:3.12.4" +"@react-aria/toggle@npm:^3.12.5": + version: 3.12.5 + resolution: "@react-aria/toggle@npm:3.12.5" dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/toggle": "npm:^3.9.4" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/toggle": "npm:^3.9.5" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c83141ba312702a977fb5a3a8676df0a8a3d376622d2f0f9cbcf4fb05a6b4e0a8654329e84b6089897132eeb4304f99c2aa62aca8f0338a5a7d772da0f3201b2 + checksum: 10/43c70eb64f13c9c9aea40e0ed5db51f36736ea3688646d47cb1e68c14f56e6473a08569806532a3aa84c7d23c21bc5a62535672e23183fd2713d57ca1e10c379 languageName: node linkType: hard -"@react-aria/toolbar@npm:3.0.0-beta.22": - version: 3.0.0-beta.22 - resolution: "@react-aria/toolbar@npm:3.0.0-beta.22" +"@react-aria/toolbar@npm:3.0.0-beta.24": + version: 3.0.0-beta.24 + resolution: "@react-aria/toolbar@npm:3.0.0-beta.24" dependencies: - "@react-aria/focus": "npm:^3.21.3" - "@react-aria/i18n": "npm:^3.12.14" - "@react-aria/utils": "npm:^3.32.0" - "@react-types/shared": "npm:^3.32.1" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/587ddaff3b11e58e4d0fb0cef59d8d7fc0481eb43f473c3cbaf6fe174a5e9ff52323636f767e2ada6b41e74ae73bab0a816846232ec1e33f35709cf91abcf49c + checksum: 10/04daf89e29c916454721d8b9462e2bfa7e1a883df2f6a3550425bf96fcd969a7bd60cef28da41aa5f24e0a34fa44fc1dd142dcedb7161476dc00aebecc78c398 languageName: node linkType: hard -"@react-aria/toolbar@npm:3.0.0-beta.23": - version: 3.0.0-beta.23 - resolution: "@react-aria/toolbar@npm:3.0.0-beta.23" +"@react-aria/tooltip@npm:^3.9.2": + version: 3.9.2 + resolution: "@react-aria/tooltip@npm:3.9.2" dependencies: - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/tooltip": "npm:^3.5.11" + "@react-types/shared": "npm:^3.33.1" + "@react-types/tooltip": "npm:^3.5.2" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/a79c553de929b49691b61c78f026c2cc672811e4dfc76d197172e9400753392bca8d42f7059d8395e3ad55584e8f1c0abd2cc4da89b1e1cda8d407725ba76cab + checksum: 10/b702c1402318dc5e020bab2d08dc63ee6d94a90d8f00efbb6ed01109cb30829b98231fdb7b940219837053c102eb02904867aff6d080b31ca5b7b0e102913799 languageName: node linkType: hard -"@react-aria/tooltip@npm:^3.9.1": - version: 3.9.1 - resolution: "@react-aria/tooltip@npm:3.9.1" +"@react-aria/tree@npm:^3.1.7": + version: 3.1.7 + resolution: "@react-aria/tree@npm:3.1.7" dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/tooltip": "npm:^3.5.10" - "@react-types/shared": "npm:^3.33.0" - "@react-types/tooltip": "npm:^3.5.1" + "@react-aria/gridlist": "npm:^3.14.4" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/tree": "npm:^3.9.6" + "@react-types/button": "npm:^3.15.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c2d24095953c59cd3768290ad3574f0f2d9af1a1604d3e926e21de9b7ae67608b0471d21e5c86f301f8274bdf9c3d414ca90da75633bc73a519f6a5cd85f34a2 + checksum: 10/fdb8451d2756c8096d65e7e433117fabdc2de7202305510188fe34f0ace88e026971cdea0970c1c827522f64e2498f6bed0cfb78e904cfa167ae243440087ed9 languageName: node linkType: hard -"@react-aria/tree@npm:^3.1.6": - version: 3.1.6 - resolution: "@react-aria/tree@npm:3.1.6" - dependencies: - "@react-aria/gridlist": "npm:^3.14.3" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/utils": "npm:^3.33.0" - "@react-stately/tree": "npm:^3.9.5" - "@react-types/button": "npm:^3.15.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/2726fbcfc6fbb06eefee2404036bb4062a7dc3400d58f2bc47452bd194ce85fd6da20293d0a59fb01e757987df3a83eb8e1a7f6480ff4f21047399cfb39f5db4 - languageName: node - linkType: hard - -"@react-aria/utils@npm:^3.32.0, @react-aria/utils@npm:^3.33.0": - version: 3.33.0 - resolution: "@react-aria/utils@npm:3.33.0" +"@react-aria/utils@npm:^3.33.1": + version: 3.33.1 + resolution: "@react-aria/utils@npm:3.33.1" dependencies: "@react-aria/ssr": "npm:^3.9.10" "@react-stately/flags": "npm:^3.1.2" "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/dea012c0fdcf0d23171ce6cf372ee91ebb965f8640b40913bbdbd4904429cf046ecc9ed952c9e7a53ade6642bc3d73a03b0b99e9b57761d2f802c14a840b3ab2 + checksum: 10/676de77e907862fc44963657a370a45242bfbec2cd54a2ed0935a709315e31415c7c045e2721f067c315418e9aa8c46aa9bd18cb6010820af357c17e335cba47 languageName: node linkType: hard -"@react-aria/virtualizer@npm:^4.1.11": - version: 4.1.11 - resolution: "@react-aria/virtualizer@npm:4.1.11" +"@react-aria/virtualizer@npm:^4.1.13": + version: 4.1.13 + resolution: "@react-aria/virtualizer@npm:4.1.13" dependencies: - "@react-aria/i18n": "npm:^3.12.14" - "@react-aria/interactions": "npm:^3.26.0" - "@react-aria/utils": "npm:^3.32.0" - "@react-stately/virtualizer": "npm:^4.4.4" - "@react-types/shared": "npm:^3.32.1" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-stately/virtualizer": "npm:^4.4.6" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c5cab6bc99affea02bc3f77618374e3c2e8e4ffa900ebee21a201acbeae1c3e24e9fe75ba810b8aa5432d36841cecddf3e9fde9886e18b9fd35c9db26f73ad24 + checksum: 10/71ad38a1c1997c9f66a16a56743dfc334f71cbf91ef0ae373d553157b784c0e5f2c2bb197fdf88475aebb178a2556ce7a33b8759a82d75dc14cb1720c608bd7a languageName: node linkType: hard -"@react-aria/visually-hidden@npm:^3.8.30": - version: 3.8.30 - resolution: "@react-aria/visually-hidden@npm:3.8.30" +"@react-aria/visually-hidden@npm:^3.8.31": + version: 3.8.31 + resolution: "@react-aria/visually-hidden@npm:3.8.31" dependencies: - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/utils": "npm:^3.33.0" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/utils": "npm:^3.33.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/27ce03bac8f1a29b80d9e34655c72494250d81fdd2005c3a85165f4d6ea99ad6a8dcdaf6ca4c08a82503f47301e24f192e28a1815146ae56c981614acb26831b + checksum: 10/22a473a153edb94ba167f6bb470a7ec864134bdd0159e6b8801d0564ea02f5e7cc32f935a80f60b38b69863d747f1c6372ffd4006010448383de156551659c8d languageName: node linkType: hard @@ -16498,139 +16605,139 @@ __metadata: languageName: node linkType: hard -"@react-stately/calendar@npm:^3.9.1, @react-stately/calendar@npm:^3.9.2": - version: 3.9.2 - resolution: "@react-stately/calendar@npm:3.9.2" +"@react-stately/calendar@npm:^3.9.3": + version: 3.9.3 + resolution: "@react-stately/calendar@npm:3.9.3" dependencies: - "@internationalized/date": "npm:^3.11.0" + "@internationalized/date": "npm:^3.12.0" "@react-stately/utils": "npm:^3.11.0" - "@react-types/calendar": "npm:^3.8.2" - "@react-types/shared": "npm:^3.33.0" + "@react-types/calendar": "npm:^3.8.3" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/76b3836242d8062a0109921e3032a9842b3a4a0ed5267a1707c49d48e4f06a02f2746da4f3b054bcb0420d923c790cdd0798e64bd18893f49d57ec216353cef8 + checksum: 10/0faaf9c74cd5ca4b09781614c0daf745f07abc865bb7f10cab605225be65a6c5c45359d9d52de2a4ec2cb84a84572b2078ed07b4da3f4981d20e7a1d95bf6653 languageName: node linkType: hard -"@react-stately/checkbox@npm:^3.7.3, @react-stately/checkbox@npm:^3.7.4": +"@react-stately/checkbox@npm:^3.7.5": + version: 3.7.5 + resolution: "@react-stately/checkbox@npm:3.7.5" + dependencies: + "@react-stately/form": "npm:^3.2.4" + "@react-stately/utils": "npm:^3.11.0" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/4bee7ba03a8bd7e4b0aeadc138ff888a5c64490ff3172dbbe21e40929501ca70317b53a3aa8c64aa6e35d442fc2c4da4297e7e8e7e8e33f900bb4c2607ba39b5 + languageName: node + linkType: hard + +"@react-stately/collections@npm:^3.12.10": + version: 3.12.10 + resolution: "@react-stately/collections@npm:3.12.10" + dependencies: + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/25b33fcef3ed21398d1b60848d49a69f45a5ff89b974e225f34363c5e2133551ed818cd4841d7a133a51bf24284e6d291739bf4e56b34845c4b12401c16e8e0d + languageName: node + linkType: hard + +"@react-stately/color@npm:^3.9.5": + version: 3.9.5 + resolution: "@react-stately/color@npm:3.9.5" + dependencies: + "@internationalized/number": "npm:^3.6.5" + "@internationalized/string": "npm:^3.2.7" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/numberfield": "npm:^3.11.0" + "@react-stately/slider": "npm:^3.7.5" + "@react-stately/utils": "npm:^3.11.0" + "@react-types/color": "npm:^3.1.4" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/901994e074ac2d2d51f7f956d1de59d4f2fa267caeaa325e8078eb2904fbd4d232cc645baba333fff47c837e3f390513a7d2d3540f4819b0a24243668cd581ec + languageName: node + linkType: hard + +"@react-stately/combobox@npm:^3.13.0": + version: 3.13.0 + resolution: "@react-stately/combobox@npm:3.13.0" + dependencies: + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/list": "npm:^3.13.4" + "@react-stately/overlays": "npm:^3.6.23" + "@react-stately/utils": "npm:^3.11.0" + "@react-types/combobox": "npm:^3.14.0" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/84188013c209229c13b2999dff3e8492629d74b74894bae290335fe824f047cf0cab46a01ddff0c0adcd0a5661b349d5e01365eeb72c35b89cb48efb20428c93 + languageName: node + linkType: hard + +"@react-stately/data@npm:^3.15.2": + version: 3.15.2 + resolution: "@react-stately/data@npm:3.15.2" + dependencies: + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/7ca81878ac240e034e16e54146c9b143c770631c5e17ad2ac5edac58ab380b41ff117309119397e8659d99515705659034d9f1dcf01e16a65366b388846c6949 + languageName: node + linkType: hard + +"@react-stately/datepicker@npm:^3.16.1": + version: 3.16.1 + resolution: "@react-stately/datepicker@npm:3.16.1" + dependencies: + "@internationalized/date": "npm:^3.12.0" + "@internationalized/number": "npm:^3.6.5" + "@internationalized/string": "npm:^3.2.7" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/overlays": "npm:^3.6.23" + "@react-stately/utils": "npm:^3.11.0" + "@react-types/datepicker": "npm:^3.13.5" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/1b35aaa10e088c004e070e6df253fd5f23cb6a02992fdbc84d4f702f65ee40d7c6ca18df633888c4d71029fc9814ef44c95330714431c046ee2e807547afaa93 + languageName: node + linkType: hard + +"@react-stately/disclosure@npm:^3.0.11": + version: 3.0.11 + resolution: "@react-stately/disclosure@npm:3.0.11" + dependencies: + "@react-stately/utils": "npm:^3.11.0" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/0aa11e08c732f6ff1a4aa8ffd72341ffceaab554c7be52cf78972e40e59ffadf5a124dc6a4355946b68fcfe1326b4c178a38ec33a4fe8539c3e0863b97b2de4a + languageName: node + linkType: hard + +"@react-stately/dnd@npm:^3.7.4": version: 3.7.4 - resolution: "@react-stately/checkbox@npm:3.7.4" + resolution: "@react-stately/dnd@npm:3.7.4" dependencies: - "@react-stately/form": "npm:^3.2.3" - "@react-stately/utils": "npm:^3.11.0" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/shared": "npm:^3.33.0" + "@react-stately/selection": "npm:^3.20.9" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/037a22d0875c88c239f879e9b385689971312f80d8021c3f266bc3428aca1ed99424e6e6ee4d6c779a9f6e1a11be19cb99960561588d0f70a4b1fb3963cf40be - languageName: node - linkType: hard - -"@react-stately/collections@npm:^3.12.8, @react-stately/collections@npm:^3.12.9": - version: 3.12.9 - resolution: "@react-stately/collections@npm:3.12.9" - dependencies: - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/06d5426f2a1e96ac079fdc7a3c53bbe1c1420949e820a8c5afe82e6b2bcf267b9d523ff20ae80fabaa5ff745aa3b383e5c44e35b7d9febf1a39bb4aeb55a76c0 - languageName: node - linkType: hard - -"@react-stately/color@npm:^3.9.3, @react-stately/color@npm:^3.9.4": - version: 3.9.4 - resolution: "@react-stately/color@npm:3.9.4" - dependencies: - "@internationalized/number": "npm:^3.6.5" - "@internationalized/string": "npm:^3.2.7" - "@react-stately/form": "npm:^3.2.3" - "@react-stately/numberfield": "npm:^3.10.4" - "@react-stately/slider": "npm:^3.7.4" - "@react-stately/utils": "npm:^3.11.0" - "@react-types/color": "npm:^3.1.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/7a345ef8e4276286c081301e035a54b7ce9168893b7b041ea41bba91409c8edc6205d2784ea3768997693f460012bb6d1805153fc70e8583a44a6cd900d11893 - languageName: node - linkType: hard - -"@react-stately/combobox@npm:^3.12.1, @react-stately/combobox@npm:^3.12.2": - version: 3.12.2 - resolution: "@react-stately/combobox@npm:3.12.2" - dependencies: - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/form": "npm:^3.2.3" - "@react-stately/list": "npm:^3.13.3" - "@react-stately/overlays": "npm:^3.6.22" - "@react-stately/utils": "npm:^3.11.0" - "@react-types/combobox": "npm:^3.13.11" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/0c1b7d4b39edbdaf64b97cf04e8564d44e974f3441b4875003e5ebad3b667cec27d22b64bfed1ed229215d646574503db3b31254efb3e855aaf0ab3b32ed5304 - languageName: node - linkType: hard - -"@react-stately/data@npm:^3.15.0": - version: 3.15.0 - resolution: "@react-stately/data@npm:3.15.0" - dependencies: - "@react-types/shared": "npm:^3.32.1" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/9aac5846536ad65b319507124825aa88043fbff72fbf0a4d4e1d52726eab6a0e876bd125f2ac03bb9e6ba4cd4ae72f2ae398662ef614e46ed34ab64775710776 - languageName: node - linkType: hard - -"@react-stately/datepicker@npm:^3.15.3, @react-stately/datepicker@npm:^3.16.0": - version: 3.16.0 - resolution: "@react-stately/datepicker@npm:3.16.0" - dependencies: - "@internationalized/date": "npm:^3.11.0" - "@internationalized/number": "npm:^3.6.5" - "@internationalized/string": "npm:^3.2.7" - "@react-stately/form": "npm:^3.2.3" - "@react-stately/overlays": "npm:^3.6.22" - "@react-stately/utils": "npm:^3.11.0" - "@react-types/datepicker": "npm:^3.13.4" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/35fd8196daa769ebbddbe34951fc666c7026d68c0f439f094b37f95138eaa8af0c25e1e5168340e7919415f122d1a4ca64b1242bd23e3e1abe1440d824adde53 - languageName: node - linkType: hard - -"@react-stately/disclosure@npm:^3.0.10, @react-stately/disclosure@npm:^3.0.9": - version: 3.0.10 - resolution: "@react-stately/disclosure@npm:3.0.10" - dependencies: - "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/a47ede0ffa3b08f0f829e5c9cfa4d623bdc425009fd686e298e096c0a1f6e058af5af6cf1f45f8c4adccf5717bcfd410364ae2e91226a7b16e67bfbd541154f4 - languageName: node - linkType: hard - -"@react-stately/dnd@npm:^3.7.2, @react-stately/dnd@npm:^3.7.3": - version: 3.7.3 - resolution: "@react-stately/dnd@npm:3.7.3" - dependencies: - "@react-stately/selection": "npm:^3.20.8" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/791b729411b29a6db8bddff6408664cce35bcfa34444ee268d399b50acdc7bd96b1fb7dcdbc5352fec3ec748b95d38ca80231808b118b8c860567f76daf9ddbf + checksum: 10/f3c2a59370f76f512f1707592a1f78c9a790b1bfdfb019a786008962c837a2b89f5ef9ef63949411876447f9bd4fe63e8be80b779050c7b3789a7031379c5a67 languageName: node linkType: hard @@ -16643,211 +16750,211 @@ __metadata: languageName: node linkType: hard -"@react-stately/form@npm:^3.2.2, @react-stately/form@npm:^3.2.3": - version: 3.2.3 - resolution: "@react-stately/form@npm:3.2.3" +"@react-stately/form@npm:^3.2.4": + version: 3.2.4 + resolution: "@react-stately/form@npm:3.2.4" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/0c13c4dc404edc9e879fbeda7f819276e7c130cbe40fd81969c25d52fd15cdd7885961a4b52d5a5f7e1375695741e855232cf4fef100a063681bcc42aea0f4c4 + checksum: 10/c8367bd4f7fcee4c098ddd5dd2c2366a1b942c0a516cc037e0e09efbcb4556abbe112893b0178cc90505ce3dc704ed58c3c9d77fa819fcda8d6929acde61d5c4 languageName: node linkType: hard -"@react-stately/grid@npm:^3.11.8": - version: 3.11.8 - resolution: "@react-stately/grid@npm:3.11.8" +"@react-stately/grid@npm:^3.11.9": + version: 3.11.9 + resolution: "@react-stately/grid@npm:3.11.9" dependencies: - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/selection": "npm:^3.20.8" - "@react-types/grid": "npm:^3.3.7" - "@react-types/shared": "npm:^3.33.0" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/selection": "npm:^3.20.9" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/073e2318c4f7e9631c88a23342965ae8a1356d7e9f3c3169874a4a991524b45773fcc9e2f58bb030ac32edc1f167c7764b75bb372a1f013fa28b647630229421 + checksum: 10/d908503207bbfa23a77bc0dcc30283d652048aee18410123a8478be960eedcfb4aea0b92390a919710b5b29487ba80e22caab2d4cd8c3d108eb2cd731d7361f1 languageName: node linkType: hard -"@react-stately/layout@npm:^4.5.2": - version: 4.5.2 - resolution: "@react-stately/layout@npm:4.5.2" +"@react-stately/layout@npm:^4.6.0": + version: 4.6.0 + resolution: "@react-stately/layout@npm:4.6.0" dependencies: - "@react-stately/collections": "npm:^3.12.8" - "@react-stately/table": "npm:^3.15.2" - "@react-stately/virtualizer": "npm:^4.4.4" - "@react-types/grid": "npm:^3.3.6" - "@react-types/shared": "npm:^3.32.1" - "@react-types/table": "npm:^3.13.4" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/table": "npm:^3.15.4" + "@react-stately/virtualizer": "npm:^4.4.6" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + "@react-types/table": "npm:^3.13.6" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/03bd63f2e2739420790c47340cc8db07e8a02d11364903d49c99a0f1a0b09fe047a667d9e656ecb2eb0b2c24aa702f05e4be7eeb7758c3fb6a525cc8139ddb9a + checksum: 10/fd72142017bb61bd5a9008a8978b02fe54886f28fe216149c502370f349ac1ad86509d0e473077566fed2e84fb4d707be8f2ac0d087b7b7491cc7e61183aaedd languageName: node linkType: hard -"@react-stately/list@npm:^3.13.2, @react-stately/list@npm:^3.13.3": - version: 3.13.3 - resolution: "@react-stately/list@npm:3.13.3" +"@react-stately/list@npm:^3.13.4": + version: 3.13.4 + resolution: "@react-stately/list@npm:3.13.4" dependencies: - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/selection": "npm:^3.20.8" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/selection": "npm:^3.20.9" "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/2ebc32408e537134838ccf1b09be90c8e64ed6f7b786456c86bb501700b56f3e8e912de0eb2cc9ebe65864ca2d72ea3f01c56de4b5bd7f6c80e3e742724838d2 + checksum: 10/eca09aae2123049ebc1094a94793bd74090fc78cd908381056199ae95fa5e9bef4d0f799b63efe04e7a39e995412e208e7da3c93f748e5e556bccdbfdaf98ad4 languageName: node linkType: hard -"@react-stately/menu@npm:^3.9.10, @react-stately/menu@npm:^3.9.9": - version: 3.9.10 - resolution: "@react-stately/menu@npm:3.9.10" +"@react-stately/menu@npm:^3.9.11": + version: 3.9.11 + resolution: "@react-stately/menu@npm:3.9.11" dependencies: - "@react-stately/overlays": "npm:^3.6.22" - "@react-types/menu": "npm:^3.10.6" - "@react-types/shared": "npm:^3.33.0" + "@react-stately/overlays": "npm:^3.6.23" + "@react-types/menu": "npm:^3.10.7" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/60e4b66e430d449977ec19ef1a0abab7ecc372fb562eae14494ffde4970b0677d010c1b49349b9a995fd009ec2590df91aa992f4a994d63cd95c522e0a5bc1b7 + checksum: 10/863a11dab298ce79c68ed8e016a989ef28a77f2647f6f394649c75b5178acd8b6f3c680d4801973ef9d6a9ae0b28434c55e592ff666243d536241fc5d435e761 languageName: node linkType: hard -"@react-stately/numberfield@npm:^3.10.3, @react-stately/numberfield@npm:^3.10.4": - version: 3.10.4 - resolution: "@react-stately/numberfield@npm:3.10.4" +"@react-stately/numberfield@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-stately/numberfield@npm:3.11.0" dependencies: "@internationalized/number": "npm:^3.6.5" - "@react-stately/form": "npm:^3.2.3" + "@react-stately/form": "npm:^3.2.4" "@react-stately/utils": "npm:^3.11.0" - "@react-types/numberfield": "npm:^3.8.17" + "@react-types/numberfield": "npm:^3.8.18" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3dc6a32e07ca2937532de5985b06ed0653c628d26048afc634c3c0e61c95fad17ae416b3ee5bee37c8b4e94225455078525d5ee51d66287396302ceca4180f4b + checksum: 10/e932b0f2e78409e89f892b0b4b807153fa82c9ebef9957e927a45249688cbd7af0d71a939f3a635e4947d45414c8c652244c3f9453286948e36953fbbdc3f31e languageName: node linkType: hard -"@react-stately/overlays@npm:^3.6.21, @react-stately/overlays@npm:^3.6.22": - version: 3.6.22 - resolution: "@react-stately/overlays@npm:3.6.22" +"@react-stately/overlays@npm:^3.6.23": + version: 3.6.23 + resolution: "@react-stately/overlays@npm:3.6.23" dependencies: "@react-stately/utils": "npm:^3.11.0" - "@react-types/overlays": "npm:^3.9.3" + "@react-types/overlays": "npm:^3.9.4" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/9d895806d4958a97c071a32b7b73bc933e04e4273616a43c40de49c2637241258e5ef9a1e53fc118f48b60e2583d60f5f6d0555caef5bf602af9c8d4811e643f + checksum: 10/bea8636bc0fe4bfc033a5ddc6d8f8d0f2dd2c8687b6d3fead535b26f908fe27c69206f95b2e0a5751a2bb9ca9a587a75ce45eb5e68bd85a3e9ce561469997d40 languageName: node linkType: hard -"@react-stately/radio@npm:^3.11.3, @react-stately/radio@npm:^3.11.4": - version: 3.11.4 - resolution: "@react-stately/radio@npm:3.11.4" +"@react-stately/radio@npm:^3.11.5": + version: 3.11.5 + resolution: "@react-stately/radio@npm:3.11.5" dependencies: - "@react-stately/form": "npm:^3.2.3" + "@react-stately/form": "npm:^3.2.4" "@react-stately/utils": "npm:^3.11.0" - "@react-types/radio": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" + "@react-types/radio": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/529d5962f9b098918b8a8f5cb69bb4644f06b7bc00476abfd7bcfcd5d7c5051f25e934ebd2db1e3b97bad285a3a6639a7a0247a4972f7657cb2a79aaee097754 + checksum: 10/6bc66989a1a3193b24f770e382e29af379b59aa1423d8ff6c8d8c3d99abefa90ea84421fecc1b52652e010cd4fc5e71494eb47ea4b6bcee22f1e67ea44ad4762 languageName: node linkType: hard -"@react-stately/searchfield@npm:^3.5.17, @react-stately/searchfield@npm:^3.5.18": - version: 3.5.18 - resolution: "@react-stately/searchfield@npm:3.5.18" +"@react-stately/searchfield@npm:^3.5.19": + version: 3.5.19 + resolution: "@react-stately/searchfield@npm:3.5.19" dependencies: "@react-stately/utils": "npm:^3.11.0" - "@react-types/searchfield": "npm:^3.6.7" + "@react-types/searchfield": "npm:^3.6.8" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/affbd80490fa24be6fe822f35f11732d45b7c3e735227b1035c0e6fbd1aa859d3941455c285afbf5ccfe28f93297891588eac2b3796db351aae866e48ff4ee88 + checksum: 10/830cd1a684d02ec67584718dd2dfdbd00bfdc94347f7e9df1b4734b47a723b5c770b7023db1c5ee863e39b5c108766b977e2fb7d0d40b6bc6c5b23a6fc09c530 languageName: node linkType: hard -"@react-stately/select@npm:^3.9.0, @react-stately/select@npm:^3.9.1": - version: 3.9.1 - resolution: "@react-stately/select@npm:3.9.1" +"@react-stately/select@npm:^3.9.2": + version: 3.9.2 + resolution: "@react-stately/select@npm:3.9.2" dependencies: - "@react-stately/form": "npm:^3.2.3" - "@react-stately/list": "npm:^3.13.3" - "@react-stately/overlays": "npm:^3.6.22" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/list": "npm:^3.13.4" + "@react-stately/overlays": "npm:^3.6.23" "@react-stately/utils": "npm:^3.11.0" - "@react-types/select": "npm:^3.12.1" - "@react-types/shared": "npm:^3.33.0" + "@react-types/select": "npm:^3.12.2" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/f890cb5e3b94705631099e6907ee42f7b6c83fc6c2f57eb29d5559d448a03ba448707e78d6cd1f450c3bdb88c9c7d386ca83187d758c5b5069c18d1bc60daba5 + checksum: 10/02e62eac27de8753e5f653e075c3aef336cf52cb28dbb68187d15a75046ed47973661dcb2e46da3044420e11eed35c54698617e39e0f18bece9f4ca97edb46f9 languageName: node linkType: hard -"@react-stately/selection@npm:^3.20.7, @react-stately/selection@npm:^3.20.8": - version: 3.20.8 - resolution: "@react-stately/selection@npm:3.20.8" +"@react-stately/selection@npm:^3.20.9": + version: 3.20.9 + resolution: "@react-stately/selection@npm:3.20.9" dependencies: - "@react-stately/collections": "npm:^3.12.9" + "@react-stately/collections": "npm:^3.12.10" "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/d84cd03233b4030ccdca9b00818a6bc3c3a508930ac3c6517ab9e9d1c5ceb15cc83ab936980a1d64dd10ace1d2bd912a3ddd216e132dd9e6838dee68904ff80c + checksum: 10/9693f3147c81ad2eecf3b29df3f9bdbde4bce89698f6e954f8d7fe49c4ea4288c11258d892e1576dfd790d03b12d3a2b8333351ffb9579e04434b8d9cc796482 languageName: node linkType: hard -"@react-stately/slider@npm:^3.7.3, @react-stately/slider@npm:^3.7.4": - version: 3.7.4 - resolution: "@react-stately/slider@npm:3.7.4" +"@react-stately/slider@npm:^3.7.5": + version: 3.7.5 + resolution: "@react-stately/slider@npm:3.7.5" dependencies: "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" - "@react-types/slider": "npm:^3.8.3" + "@react-types/shared": "npm:^3.33.1" + "@react-types/slider": "npm:^3.8.4" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/25d07bd74ddc052c10a0bf3725adf2d94eb429d9fe993e36fd141b823bfd41a78e6438628ea835fb8101bd569cb968bf1f7f3e6f2db1646b92c089592b301e40 + checksum: 10/a54e9aca9a1f4b5a84fe7ed49fbc07ecc2f9c4611a05e2a15751c38787901862cd3dda43009c3413c1200b9a979f2060c76f7623819fdfd94807ee3d2607142d languageName: node linkType: hard -"@react-stately/table@npm:^3.15.2, @react-stately/table@npm:^3.15.3": - version: 3.15.3 - resolution: "@react-stately/table@npm:3.15.3" +"@react-stately/table@npm:^3.15.4": + version: 3.15.4 + resolution: "@react-stately/table@npm:3.15.4" dependencies: - "@react-stately/collections": "npm:^3.12.9" + "@react-stately/collections": "npm:^3.12.10" "@react-stately/flags": "npm:^3.1.2" - "@react-stately/grid": "npm:^3.11.8" - "@react-stately/selection": "npm:^3.20.8" + "@react-stately/grid": "npm:^3.11.9" + "@react-stately/selection": "npm:^3.20.9" "@react-stately/utils": "npm:^3.11.0" - "@react-types/grid": "npm:^3.3.7" - "@react-types/shared": "npm:^3.33.0" - "@react-types/table": "npm:^3.13.5" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + "@react-types/table": "npm:^3.13.6" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/1c3c8621ddf4965770ec70b3b7596b44f3d384e5ba9ce7a3f44095b75fecc265a766faeab37d9e3e9404ebbaf382b62378ed529f36659194f018ae49efcdd36a + checksum: 10/27b85d7d4541b1d9337c999362eeee21f6656bd7b278ba5419bf1a1c7441f9ff7a682ce87c438c5c8329351af261352867f0bbc057838971913e098ce9653e38 languageName: node linkType: hard -"@react-stately/tabs@npm:^3.8.7, @react-stately/tabs@npm:^3.8.8": - version: 3.8.8 - resolution: "@react-stately/tabs@npm:3.8.8" +"@react-stately/tabs@npm:^3.8.9": + version: 3.8.9 + resolution: "@react-stately/tabs@npm:3.8.9" dependencies: - "@react-stately/list": "npm:^3.13.3" - "@react-types/shared": "npm:^3.33.0" - "@react-types/tabs": "npm:^3.3.21" + "@react-stately/list": "npm:^3.13.4" + "@react-types/shared": "npm:^3.33.1" + "@react-types/tabs": "npm:^3.3.22" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/b149e27c839221baa922bfc33f54e17f45e654fe461a774a2241d01f05458b1ccd081f89c7e90cd658b4807233b875afab0d41d7fc6e320f62de35ac4fd54323 + checksum: 10/bfb7386c7d648d2152c46a1ef607f2c8caac3a940b7ee94033d4ec655be7ebee8f59d46cc253e012681d04476f97aac794f2269c08e927d2ecf87c1b220539d4 languageName: node linkType: hard @@ -16863,45 +16970,45 @@ __metadata: languageName: node linkType: hard -"@react-stately/toggle@npm:^3.9.3, @react-stately/toggle@npm:^3.9.4": - version: 3.9.4 - resolution: "@react-stately/toggle@npm:3.9.4" - dependencies: - "@react-stately/utils": "npm:^3.11.0" - "@react-types/checkbox": "npm:^3.10.3" - "@react-types/shared": "npm:^3.33.0" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/78286f1e82e0c14055a6180c99439d999986086dbea325b4a51ffe9ceee1051671b3d5e8bb3fe09af8448ff25c3ede47a4bbbe3f9932ebfc4b8af0010f9ed55d - languageName: node - linkType: hard - -"@react-stately/tooltip@npm:^3.5.10, @react-stately/tooltip@npm:^3.5.9": - version: 3.5.10 - resolution: "@react-stately/tooltip@npm:3.5.10" - dependencies: - "@react-stately/overlays": "npm:^3.6.22" - "@react-types/tooltip": "npm:^3.5.1" - "@swc/helpers": "npm:^0.5.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/cae1cf257ec4b8dce699ae4fbb0d817aad13818b943dc8bbd09b7d4decab534d7dcff7dcf760f738ce4df4dc2ce760913e7b1988a55c460f4c61708a2f567049 - languageName: node - linkType: hard - -"@react-stately/tree@npm:^3.9.4, @react-stately/tree@npm:^3.9.5": +"@react-stately/toggle@npm:^3.9.5": version: 3.9.5 - resolution: "@react-stately/tree@npm:3.9.5" + resolution: "@react-stately/toggle@npm:3.9.5" dependencies: - "@react-stately/collections": "npm:^3.12.9" - "@react-stately/selection": "npm:^3.20.8" "@react-stately/utils": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" + "@react-types/checkbox": "npm:^3.10.4" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/929b2be2af99ba05a0a42e724fd5c99e06c132c11ffd0522d7bd654dafa1460d13b0a463c557a45dec50f1eaa304ecbadf07e5253860f37580ce40cdc89d686e + checksum: 10/a034d2754e271eebdd6e80c530d1ba788f2c52eda2997970f646276d6a9328b7e86e5b6876473615d416e3ed774dd1adb7356b1460daa6100359473219f04236 + languageName: node + linkType: hard + +"@react-stately/tooltip@npm:^3.5.11": + version: 3.5.11 + resolution: "@react-stately/tooltip@npm:3.5.11" + dependencies: + "@react-stately/overlays": "npm:^3.6.23" + "@react-types/tooltip": "npm:^3.5.2" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/5e006f8ff866af75666b098806579e2469fe02207bf47c9c847832cc1c4417777e08200589c3a81d2f8c41a1a47db76e183750e790f4e4fda9e2df6bd462e98f + languageName: node + linkType: hard + +"@react-stately/tree@npm:^3.9.6": + version: 3.9.6 + resolution: "@react-stately/tree@npm:3.9.6" + dependencies: + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/selection": "npm:^3.20.9" + "@react-stately/utils": "npm:^3.11.0" + "@react-types/shared": "npm:^3.33.1" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a073aeea043302dbec4d4581f5efa96f5fc6627695d553af7f2f307a16a181abe0ba530111b7dde7c592794ce733c0b59af04ed7c3dc1255bf0f67e5572004df languageName: node linkType: hard @@ -16916,335 +17023,335 @@ __metadata: languageName: node linkType: hard -"@react-stately/virtualizer@npm:^4.4.4": - version: 4.4.4 - resolution: "@react-stately/virtualizer@npm:4.4.4" +"@react-stately/virtualizer@npm:^4.4.6": + version: 4.4.6 + resolution: "@react-stately/virtualizer@npm:4.4.6" dependencies: - "@react-types/shared": "npm:^3.32.1" + "@react-types/shared": "npm:^3.33.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c9d8d4b34250b8c0e91811bc618c654bac3e8eabe7a8ec119abc8f5dbbfa19faa4b31575eb0775773fda0f533fc2835e9a233b4ed0e4ae1835ddb5f0521a18aa + checksum: 10/47d8e549b638a6b37badac1bf4a1b2027be38c7121404cbc21c8afbca28131bb69076915944eafdf09246999dac169a5fb966f783c36d87bc337f2e29e2957c9 languageName: node linkType: hard -"@react-types/autocomplete@npm:3.0.0-alpha.36": - version: 3.0.0-alpha.36 - resolution: "@react-types/autocomplete@npm:3.0.0-alpha.36" +"@react-types/autocomplete@npm:3.0.0-alpha.38": + version: 3.0.0-alpha.38 + resolution: "@react-types/autocomplete@npm:3.0.0-alpha.38" dependencies: - "@react-types/combobox": "npm:^3.13.10" - "@react-types/searchfield": "npm:^3.6.6" - "@react-types/shared": "npm:^3.32.1" + "@react-types/combobox": "npm:^3.14.0" + "@react-types/searchfield": "npm:^3.6.8" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/054b3d186d50f02fbd48e5370a447a3cfaafecf3aaddc04cdb0411cf9e170d11ceec5ca6f8fb654fbd32a65a74ae52c7a900af6ebe3683ad94f36860d4e8ee8f + checksum: 10/4134c885eb90bf73b268fa5d8ed67f076ab0eccd76843c530665e1dcc1063bbb5d7ac1a88a57e2cd4866f90c908e90f5d1d9b51753873b5e3d432ab2a3403d51 languageName: node linkType: hard -"@react-types/breadcrumbs@npm:^3.7.18": - version: 3.7.18 - resolution: "@react-types/breadcrumbs@npm:3.7.18" +"@react-types/breadcrumbs@npm:^3.7.19": + version: 3.7.19 + resolution: "@react-types/breadcrumbs@npm:3.7.19" dependencies: - "@react-types/link": "npm:^3.6.6" - "@react-types/shared": "npm:^3.33.0" + "@react-types/link": "npm:^3.6.7" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/cb6933d3c0f57f4062b89ef2ceb39fa38c6b1067e916de36aaa89ebf511585cbeff2c3ce697a1099fb4f076c3fc2df2af09614b30517845863cb87f19d7383ba + checksum: 10/872c3af2ce541dd17180f94e1ceadcd99018463bf63d6a418d80c6e8a36d271633cf8f55c9468d102bde182b3e58450fcd260de53d44fff8dd9f493330270bb6 languageName: node linkType: hard -"@react-types/button@npm:^3.14.1, @react-types/button@npm:^3.15.0": - version: 3.15.0 - resolution: "@react-types/button@npm:3.15.0" +"@react-types/button@npm:^3.15.1": + version: 3.15.1 + resolution: "@react-types/button@npm:3.15.1" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c19802094ddd86b761157260eb89d772256559387111a7ee6d139e457f847ca75be0f2aa55c9132923279c7da52f8d2097f0cf0e343b84bcdc877915e3cd45cc + checksum: 10/aa58f33a7f66efc9e2fbfc329853dfa1a2cb840c1de6d0dc54f1e9afd04465fabf24aa513b83871503a089dc7a92a720e16a707300a0a53995e32d509e3b36e7 languageName: node linkType: hard -"@react-types/calendar@npm:^3.8.2": - version: 3.8.2 - resolution: "@react-types/calendar@npm:3.8.2" - dependencies: - "@internationalized/date": "npm:^3.11.0" - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/a82dd7f9886e9b1b2abf50c17acf2e94e707033468ffb426ee8634bea37b575c66bbd299cbafa9aae3723b03fd358c41b8a15d104bcd018de1d258a6ad3a29f3 - languageName: node - linkType: hard - -"@react-types/checkbox@npm:^3.10.3": - version: 3.10.3 - resolution: "@react-types/checkbox@npm:3.10.3" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/7f5b8b0617648d4fff9f74c2d2bb5adf14813fee3e73443ba76dbc1276b57374ccfa18f2dc001ea560ff827f36a1cf3c0f29a255a21538239abd520b6f357189 - languageName: node - linkType: hard - -"@react-types/color@npm:^3.1.3": - version: 3.1.3 - resolution: "@react-types/color@npm:3.1.3" - dependencies: - "@react-types/shared": "npm:^3.33.0" - "@react-types/slider": "npm:^3.8.3" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/c7e1239015d53fa125e69947cfed1fba5df5031754c5bb772bab810d04bb2fe09ab8810ebd8f273887c123060c00b9b3d5c1a21fb555077f1fc9ff3061774eb7 - languageName: node - linkType: hard - -"@react-types/combobox@npm:^3.13.10, @react-types/combobox@npm:^3.13.11": - version: 3.13.11 - resolution: "@react-types/combobox@npm:3.13.11" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/0a9482b13f9d44c8e1fd5540ac0c1bd4d629fbe15a16580471a0409854f41d5d18eb89c6fd46fc31e348b029bdd184707d6872e7c1caac184e01f522cf4bfffb - languageName: node - linkType: hard - -"@react-types/datepicker@npm:^3.13.4": - version: 3.13.4 - resolution: "@react-types/datepicker@npm:3.13.4" - dependencies: - "@internationalized/date": "npm:^3.11.0" - "@react-types/calendar": "npm:^3.8.2" - "@react-types/overlays": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/23c64399e8eaa86678057965611089290d3045cde2c7c273c4c1c8a11f643ba12ac3068df4a791859fc096312d031ae198775ddae628a7c030344eca8e233dae - languageName: node - linkType: hard - -"@react-types/dialog@npm:^3.5.23": - version: 3.5.23 - resolution: "@react-types/dialog@npm:3.5.23" - dependencies: - "@react-types/overlays": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/cafde5d276e39c2ff32919404c03eb29f67bef9f176fbcc6027ca8f67e06f8005505ad88897bb86f2ba3de2c9ac3735f89fcf266ad5eb7da9cf819b14f0a1a67 - languageName: node - linkType: hard - -"@react-types/form@npm:^3.7.16": - version: 3.7.16 - resolution: "@react-types/form@npm:3.7.16" - dependencies: - "@react-types/shared": "npm:^3.32.1" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3e8f156318746d31ef7470d366dcc1004f3a59b1a3a3f9e54331921a4b251ff51c6bf76a32be229f6d8c524b5b1292f3c1135597de6b0f52bae2d8c32a542a74 - languageName: node - linkType: hard - -"@react-types/grid@npm:^3.3.6, @react-types/grid@npm:^3.3.7": - version: 3.3.7 - resolution: "@react-types/grid@npm:3.3.7" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/6eb15fb0482b09b585aff833421b696da5a262ba781276d7573825fdd59814d700463b1d57f7b62ba0a1f5dd59a49cb5ac20aa5e3125fc2b71d316cfae138981 - languageName: node - linkType: hard - -"@react-types/link@npm:^3.6.6": - version: 3.6.6 - resolution: "@react-types/link@npm:3.6.6" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/897bbe97e3a09fb93fa3300fd4e6fd41effa33024814091cb84026c5d0f10ee3fac476a5eb81c25e291d14f4fa4c1590d1b285716f41356a4b5a254807630018 - languageName: node - linkType: hard - -"@react-types/listbox@npm:^3.7.5": - version: 3.7.5 - resolution: "@react-types/listbox@npm:3.7.5" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e91a85b6f841279180e625118a275c5c48dc2dd00f8cb38951f0adf9ca9674ac86693b85e5c9b13b08a66851bebd7a750833235723d34d3cf7cd2ae104d978b0 - languageName: node - linkType: hard - -"@react-types/menu@npm:^3.10.6": - version: 3.10.6 - resolution: "@react-types/menu@npm:3.10.6" - dependencies: - "@react-types/overlays": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/7858d6d17140d91141cb58254abcdd6b73e89d4c725b996313410fbed72889933e4d4b52db44cbf2711e22b67ec55ceaaa211f18bf847b5597b0051f63a19f85 - languageName: node - linkType: hard - -"@react-types/meter@npm:^3.4.14": - version: 3.4.14 - resolution: "@react-types/meter@npm:3.4.14" - dependencies: - "@react-types/progress": "npm:^3.5.17" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/fc8ab1a1696492c3ec535b4237e055f3cdb9d76f7e4089feb5189b8b5e23cdebd9d4ccbb60f8950574951e8faee1be39da184ff83e366d75673ab4b9d0e82537 - languageName: node - linkType: hard - -"@react-types/numberfield@npm:^3.8.17": - version: 3.8.17 - resolution: "@react-types/numberfield@npm:3.8.17" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/bad62e5773989e4cd88818b2fdf2f5f461b8daeba4e565f054995e22780703700e3eb84c6161193bf16fd49f79dc72315d51d979b28ed83c2229f9ed6f4cd048 - languageName: node - linkType: hard - -"@react-types/overlays@npm:^3.9.3": - version: 3.9.3 - resolution: "@react-types/overlays@npm:3.9.3" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/0c778a1c466613a6d03fb49419fada66a40a0c297e41a492ae9832b3e17c0e24325f2faa865e3917071c62cf12a9744f3fa84102f31ee3f86283c38ca3447de1 - languageName: node - linkType: hard - -"@react-types/progress@npm:^3.5.17": - version: 3.5.17 - resolution: "@react-types/progress@npm:3.5.17" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/4fd31bf8467e8b65aef30fc240d7b87a07849446467c9ca71cf725b44a04438dd4f10360f7def957001887082e34e2fa3cee3854e1ed22a1f82edf4cda31cae2 - languageName: node - linkType: hard - -"@react-types/radio@npm:^3.9.3": - version: 3.9.3 - resolution: "@react-types/radio@npm:3.9.3" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/9c9bf31ab7f5631d7be2302d5960b197c7cd7736671b56f0c192bf728ecfacb193d6e5f7f6c8bf2b546b0ffa780aeb6bb70f570d62d9549871a078e72aa54fb6 - languageName: node - linkType: hard - -"@react-types/searchfield@npm:^3.6.6, @react-types/searchfield@npm:^3.6.7": - version: 3.6.7 - resolution: "@react-types/searchfield@npm:3.6.7" - dependencies: - "@react-types/shared": "npm:^3.33.0" - "@react-types/textfield": "npm:^3.12.7" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/34eb78e0b83fa66e9f1b6c3205f97ea4626d4392742393279291c26c8b48d0c6f2c0eb9af09db02f3fa3a6d0d8b80c629d8344981711cb906cb45d502a1f67e0 - languageName: node - linkType: hard - -"@react-types/select@npm:^3.12.1": - version: 3.12.1 - resolution: "@react-types/select@npm:3.12.1" - dependencies: - "@react-types/shared": "npm:^3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/9691920074c2a599033fa17ebfc44ad58b2089c3073c203f6d09c42ccf0ad2d87d660e72f4b641b3d13f1fe2a93dcd6202f79b52ac9766739c70d3150ec4f138 - languageName: node - linkType: hard - -"@react-types/shared@npm:^3.32.1, @react-types/shared@npm:^3.33.0": - version: 3.33.0 - resolution: "@react-types/shared@npm:3.33.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e2bbb014e03b71d7b5743d0ec0754e213dd954724fe8dcdbdca2af752eb59cfac6380f561ee3a7bcef3935963b18f086305994be93936dbc9894cbe18ab22850 - languageName: node - linkType: hard - -"@react-types/slider@npm:^3.8.3": +"@react-types/calendar@npm:^3.8.3": version: 3.8.3 - resolution: "@react-types/slider@npm:3.8.3" + resolution: "@react-types/calendar@npm:3.8.3" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@internationalized/date": "npm:^3.12.0" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/2acc47d43bd42d643b1cdf8dd4ca7abc96850b32f61f13997a4ae615fdded990e789b7ee7a30495373f97c71ed89d1f20013de5387ec2689bf046fac72b6c304 + checksum: 10/92c40145eab9c43cc490dfd8dadcc5f39e167914c4822db5c00c5267603fdb310e50781998e685fdf52a650b97bcb02dabdd1ae50b2a8faa95d7e37f6c2d3e41 languageName: node linkType: hard -"@react-types/switch@npm:^3.5.16": - version: 3.5.16 - resolution: "@react-types/switch@npm:3.5.16" +"@react-types/checkbox@npm:^3.10.4": + version: 3.10.4 + resolution: "@react-types/checkbox@npm:3.10.4" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/293f72837e807425149405c71be90b32fdc55c46a01f1d03580ed24c410a76f053b4e49d9270f4db6e0e7c4587e0b6b0c3b7c7eca4b0518dfe31102b56cfbf44 + checksum: 10/c87898a75cc751c7adeba2c684a1b346808e76c5f6da3e84980d3540c1dd5a5520ee24caa4e06f855bb9bd460fd64c3db79fdace6ca1b461ff1d04b3b5532e9d languageName: node linkType: hard -"@react-types/table@npm:^3.13.4, @react-types/table@npm:^3.13.5": +"@react-types/color@npm:^3.1.4": + version: 3.1.4 + resolution: "@react-types/color@npm:3.1.4" + dependencies: + "@react-types/shared": "npm:^3.33.1" + "@react-types/slider": "npm:^3.8.4" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/999466eda395092c5ed7f65911c6df764088bb102b7f40e380691baf19d8d272bfb6438ba222bfab22d49ee6e8d26840c18daca5112b9b45a3d95555f0a26a28 + languageName: node + linkType: hard + +"@react-types/combobox@npm:^3.14.0": + version: 3.14.0 + resolution: "@react-types/combobox@npm:3.14.0" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/950b8c945ea15af1420196e3905fd1982685ff6afbd6aa998a4e6792c9e07d314d988778ed5e480024525d0b5ddef3b741631066e6bda0978faf199984875074 + languageName: node + linkType: hard + +"@react-types/datepicker@npm:^3.13.5": version: 3.13.5 - resolution: "@react-types/table@npm:3.13.5" + resolution: "@react-types/datepicker@npm:3.13.5" dependencies: - "@react-types/grid": "npm:^3.3.7" - "@react-types/shared": "npm:^3.33.0" + "@internationalized/date": "npm:^3.12.0" + "@react-types/calendar": "npm:^3.8.3" + "@react-types/overlays": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/e9bc2a4476cd44b38d4613df5702f9e46fcc6c5656c426c5ee53d8175be75f9fd9241c5c8e93ce2c0945e20b21ee80cdf917ae75ee0410f6d9cbc0320833c7fc + checksum: 10/13ecd4931572e58e22cb79fca91690b97ef84525754f9367b78e60b1692ae8f3f21c52bd9a0de648c6acfac89ab0c1ed706d4e20e9246c4ab2b695af1567606d languageName: node linkType: hard -"@react-types/tabs@npm:^3.3.21": - version: 3.3.21 - resolution: "@react-types/tabs@npm:3.3.21" +"@react-types/dialog@npm:^3.5.24": + version: 3.5.24 + resolution: "@react-types/dialog@npm:3.5.24" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@react-types/overlays": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/603a1ff81ae8b58cd069c95af326b96095245a873db4d73d531571417e7e56ca1ec5ee82bd49390c96679e0aab61d146dbe9a8b085cff0d2f538b9d6a987596c + checksum: 10/fbfc265b1ea0105b3815eba7fc05898f0b0fd0d6b9d21d3a38abfc579ce01031bfcfc1cab9a3ffaf781a0842498a2faf33a0b04ca80983eab21bf0f08d76680f languageName: node linkType: hard -"@react-types/textfield@npm:^3.12.7": - version: 3.12.7 - resolution: "@react-types/textfield@npm:3.12.7" +"@react-types/form@npm:^3.7.18": + version: 3.7.18 + resolution: "@react-types/form@npm:3.7.18" dependencies: - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/3e217c42d8ff9fff01c04a806ed9114a2672d152f1cf194fcaa1fa932b24437b00cdaa16c5d178fc64201f5386b0d052f126f6df2fd3e0f4d8747c62976eace0 + checksum: 10/af39ed916115da5f11c895a92ed77922bcb282dc1cb0c47d3fc39caddaa900d5fdbdc753065ab52d2475f26ecf05dfa26d5e3da3459c57fa86ac6d4311092136 languageName: node linkType: hard -"@react-types/tooltip@npm:^3.5.1": - version: 3.5.1 - resolution: "@react-types/tooltip@npm:3.5.1" +"@react-types/grid@npm:^3.3.8": + version: 3.3.8 + resolution: "@react-types/grid@npm:3.3.8" dependencies: - "@react-types/overlays": "npm:^3.9.3" - "@react-types/shared": "npm:^3.33.0" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/d1ab9c0ba0eddea8ae264aaee633af3af033adda72311e8a7dde13491640f4a567c3f88ba2f4d2ecd580847fa74fd13e2d9b69105e7c7d90f7f44274d012c0cd + checksum: 10/7330c7f502c5a45c29984dc21ed05fca03bcaccd13ba6f58f1185731e671c5d5860523d2da9b17065a95b24aa42cbfa17df3ade8b848efc536d73c1cd00a5a1b + languageName: node + linkType: hard + +"@react-types/link@npm:^3.6.7": + version: 3.6.7 + resolution: "@react-types/link@npm:3.6.7" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/2fd73277820df22b0a4cdee253f1e01bc57d2c7778bc0a4ab8181076a22e6feb02b08b5b358578bb4608e0a8c46ff9a871e5a670262e887b5fe008a20608acc2 + languageName: node + linkType: hard + +"@react-types/listbox@npm:^3.7.6": + version: 3.7.6 + resolution: "@react-types/listbox@npm:3.7.6" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/0580910cca9939fe9eef63dd5088408273ff6a358e1c3b6f27d7ac72b08b1456315b4bc41ea0d2e5b22375441ed4d8b1786559b3fc87fe01fb6304a07ecd571c + languageName: node + linkType: hard + +"@react-types/menu@npm:^3.10.7": + version: 3.10.7 + resolution: "@react-types/menu@npm:3.10.7" + dependencies: + "@react-types/overlays": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/bc1adcbbec0c719ccf2ed4e49106a644dae0b777d62edea58012a899207cbf3543fff2e5236826feedfdb5ea653244f61b44201714fa1e92eab1619fd3ff67d4 + languageName: node + linkType: hard + +"@react-types/meter@npm:^3.4.15": + version: 3.4.15 + resolution: "@react-types/meter@npm:3.4.15" + dependencies: + "@react-types/progress": "npm:^3.5.18" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a31fc132bec20267ffbd7bea429bcdd37ea4548ccde7ffaa578613ff1298bd5413f46bf2c86c3737d5aef420175545267ffb628b2d5a45dc8563a62a5062226d + languageName: node + linkType: hard + +"@react-types/numberfield@npm:^3.8.18": + version: 3.8.18 + resolution: "@react-types/numberfield@npm:3.8.18" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/a6bbf7c2a022423c1d99ee4a0262581eb36887962cb6f0d592a10c9854fcd74d75eee1a701eb185d6d86062b63c5682c5ec3f97907cbfeca80d2698554795dff + languageName: node + linkType: hard + +"@react-types/overlays@npm:^3.9.4": + version: 3.9.4 + resolution: "@react-types/overlays@npm:3.9.4" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/720ca12182243c8796681d3046bce5798f3edaa3dcafb8670a1a5b9254dffa035323dd50533d3b2e102b5418b4563aab91eaa34d728011fbbf4501d2ce79920c + languageName: node + linkType: hard + +"@react-types/progress@npm:^3.5.18": + version: 3.5.18 + resolution: "@react-types/progress@npm:3.5.18" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/8589f11ba9fa320da82a0b4751bc8576b9153a70facc6986fadf05156d393867f7086667c528e1880eddb51510ac7f026bffb0c667f4af2602c4fcedc264bc05 + languageName: node + linkType: hard + +"@react-types/radio@npm:^3.9.4": + version: 3.9.4 + resolution: "@react-types/radio@npm:3.9.4" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/3b36b410c1ad64ce3f3e7b524d0216faa317584ebdeeb18aced3f8960fa5efa522890bdaa170c302d8335b89d2a78a7cef8f417f676c52a21bac5d0644a9dbf6 + languageName: node + linkType: hard + +"@react-types/searchfield@npm:^3.6.8": + version: 3.6.8 + resolution: "@react-types/searchfield@npm:3.6.8" + dependencies: + "@react-types/shared": "npm:^3.33.1" + "@react-types/textfield": "npm:^3.12.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/db38ca5c20593bbe4f0e7813aa8d967b3f5ff4031a974066e7db63b9561a2fb930836c631f7e37bfd04cfceff26bd9cc6f63a0805152f289a0bf55d5b7f62f03 + languageName: node + linkType: hard + +"@react-types/select@npm:^3.12.2": + version: 3.12.2 + resolution: "@react-types/select@npm:3.12.2" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/836bbb555ba491d9256743d8a3f8e9ff52f3ae7d93ad2fa37069d9de5b516ad8edffa4f7444a60cfeb5ed0c59d2ca5c6e6a8b0a14de0066c48c6164068de0d9e + languageName: node + linkType: hard + +"@react-types/shared@npm:^3.33.1": + version: 3.33.1 + resolution: "@react-types/shared@npm:3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/18b9586e4d6f1fda82a471bde7e4dec5bcc95d1a7ece58864fe40bcd9dce175b5c76c4285d8aeed1beba2d397e3e89b521519a917aa97e16847edfa3c67b5df5 + languageName: node + linkType: hard + +"@react-types/slider@npm:^3.8.4": + version: 3.8.4 + resolution: "@react-types/slider@npm:3.8.4" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/9c35a288e37845bd7448f0994dcbcf4cb8d13085ba0b5483aabb9a9df18119b03d47bb72ff855d69b71658ee1b8b1b09244a5f3932d6077f03e35c555129a5ae + languageName: node + linkType: hard + +"@react-types/switch@npm:^3.5.17": + version: 3.5.17 + resolution: "@react-types/switch@npm:3.5.17" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/3b677ae7645a99a18ba1683dd7cac28d9a77073f715b54b1c549ae931138adda4d581578aec9173de90e8df65143e6067746d350bb1bd74423fe0f73caec5905 + languageName: node + linkType: hard + +"@react-types/table@npm:^3.13.6": + version: 3.13.6 + resolution: "@react-types/table@npm:3.13.6" + dependencies: + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/18d5acbf10d7eba5582b71848593f9198519b4b54014974ad66a94a166d4e959390358f778dd6d4e17981e87fa4822c144cc025fb13d5d2395a11f1862273f13 + languageName: node + linkType: hard + +"@react-types/tabs@npm:^3.3.22": + version: 3.3.22 + resolution: "@react-types/tabs@npm:3.3.22" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/2699b0c631a77b10c84a088f0c5452735dc1b30185578f4cc46bfab0c5b2b11fc96902f8596029890280b6102397e45b04868cc74f518818cc21b0e4a2ca2ee4 + languageName: node + linkType: hard + +"@react-types/textfield@npm:^3.12.8": + version: 3.12.8 + resolution: "@react-types/textfield@npm:3.12.8" + dependencies: + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/55c426373168e2d288bc5ae051f2e8016db67f19cdff54194ff5a477799fd74b560362b36badd0071a48a73d41f25ce68c21010bcfcb26bc9e88fc26d137b8ab + languageName: node + linkType: hard + +"@react-types/tooltip@npm:^3.5.2": + version: 3.5.2 + resolution: "@react-types/tooltip@npm:3.5.2" + dependencies: + "@react-types/overlays": "npm:^3.9.4" + "@react-types/shared": "npm:^3.33.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + checksum: 10/d20b25a73efaff881b896e8edcb16faa3fc03437935da458a99bd0781fcb17644903c94949f8ee7067f51d1018e3134171886dffce08b79221b2903635cc590b languageName: node linkType: hard @@ -17274,18 +17381,18 @@ __metadata: linkType: hard "@remixicon/react@npm:^4.6.0": - version: 4.8.0 - resolution: "@remixicon/react@npm:4.8.0" + version: 4.9.0 + resolution: "@remixicon/react@npm:4.9.0" peerDependencies: react: ">=18.2.0" - checksum: 10/10241f2e07826ce7d595cf9687a35c96cc9cf9eb68a9431d7dfa1b9df479dbef302874d28c0502cee2a536cf34722de7c49ed12d10a2b071e4e42ba4a278c516 + checksum: 10/3d8f1d86b2bb20ab5e44d15f18811e928b0886f7710eb7a1516afb9913ba72e46facec5dfee382825139d800bcbb6704c15d0c760d0f977c12257d4af8db3295 languageName: node linkType: hard -"@repeaterjs/repeater@npm:^3.0.4": - version: 3.0.5 - resolution: "@repeaterjs/repeater@npm:3.0.5" - checksum: 10/7478df13bd4631729021b2f43524fe71a4ed04b3c1c2125315078e3954f059f2ff4da5776f9be8f76008df9849e866e5ec56120f41b8bf66d2ec1a7c7bc53229 +"@repeaterjs/repeater@npm:^3.0.4, @repeaterjs/repeater@npm:^3.0.6": + version: 3.0.6 + resolution: "@repeaterjs/repeater@npm:3.0.6" + checksum: 10/25698e822847b776006428f31e2d31fbcb4faccf30c1c8d68d6e1308e58b49afb08764d1dd15536ddd67775cd01fd6c2fb22f039c05a71865448fbcfb2246af2 languageName: node linkType: hard @@ -17583,92 +17690,92 @@ __metadata: languageName: node linkType: hard -"@rspack/binding-darwin-arm64@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-darwin-arm64@npm:1.7.5" +"@rspack/binding-darwin-arm64@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-darwin-arm64@npm:1.7.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-darwin-x64@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-darwin-x64@npm:1.7.5" +"@rspack/binding-darwin-x64@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-darwin-x64@npm:1.7.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rspack/binding-linux-arm64-gnu@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.5" +"@rspack/binding-linux-arm64-gnu@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-arm64-musl@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.5" +"@rspack/binding-linux-arm64-musl@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rspack/binding-linux-x64-gnu@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.5" +"@rspack/binding-linux-x64-gnu@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-x64-musl@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-linux-x64-musl@npm:1.7.5" +"@rspack/binding-linux-x64-musl@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-linux-x64-musl@npm:1.7.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rspack/binding-wasm32-wasi@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-wasm32-wasi@npm:1.7.5" +"@rspack/binding-wasm32-wasi@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-wasm32-wasi@npm:1.7.7" dependencies: "@napi-rs/wasm-runtime": "npm:1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@rspack/binding-win32-arm64-msvc@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.5" +"@rspack/binding-win32-arm64-msvc@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-win32-ia32-msvc@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.5" +"@rspack/binding-win32-ia32-msvc@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.7" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rspack/binding-win32-x64-msvc@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.5" +"@rspack/binding-win32-x64-msvc@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rspack/binding@npm:1.7.5": - version: 1.7.5 - resolution: "@rspack/binding@npm:1.7.5" +"@rspack/binding@npm:1.7.7": + version: 1.7.7 + resolution: "@rspack/binding@npm:1.7.7" dependencies: - "@rspack/binding-darwin-arm64": "npm:1.7.5" - "@rspack/binding-darwin-x64": "npm:1.7.5" - "@rspack/binding-linux-arm64-gnu": "npm:1.7.5" - "@rspack/binding-linux-arm64-musl": "npm:1.7.5" - "@rspack/binding-linux-x64-gnu": "npm:1.7.5" - "@rspack/binding-linux-x64-musl": "npm:1.7.5" - "@rspack/binding-wasm32-wasi": "npm:1.7.5" - "@rspack/binding-win32-arm64-msvc": "npm:1.7.5" - "@rspack/binding-win32-ia32-msvc": "npm:1.7.5" - "@rspack/binding-win32-x64-msvc": "npm:1.7.5" + "@rspack/binding-darwin-arm64": "npm:1.7.7" + "@rspack/binding-darwin-x64": "npm:1.7.7" + "@rspack/binding-linux-arm64-gnu": "npm:1.7.7" + "@rspack/binding-linux-arm64-musl": "npm:1.7.7" + "@rspack/binding-linux-x64-gnu": "npm:1.7.7" + "@rspack/binding-linux-x64-musl": "npm:1.7.7" + "@rspack/binding-wasm32-wasi": "npm:1.7.7" + "@rspack/binding-win32-arm64-msvc": "npm:1.7.7" + "@rspack/binding-win32-ia32-msvc": "npm:1.7.7" + "@rspack/binding-win32-x64-msvc": "npm:1.7.7" dependenciesMeta: "@rspack/binding-darwin-arm64": optional: true @@ -17690,23 +17797,23 @@ __metadata: optional: true "@rspack/binding-win32-x64-msvc": optional: true - checksum: 10/3e66805d4dae5f2051f10c5a9126e5bf25926d2a6ccb1c794af2aa49c15e4fdcb9e362bd015a9afef1e788a3272dfe7a28a3c866713badda34579896d736ed4f + checksum: 10/c860c3318f4f3d79b6a55717e8e01a515cb7bc67ee30ebdedcdcf81b69c6885667bd0b558da89079f6f7260cc355d10388bb8c7482471f5ab768fb64d0161673 languageName: node linkType: hard "@rspack/core@npm:^1.4.11": - version: 1.7.5 - resolution: "@rspack/core@npm:1.7.5" + version: 1.7.7 + resolution: "@rspack/core@npm:1.7.7" dependencies: "@module-federation/runtime-tools": "npm:0.22.0" - "@rspack/binding": "npm:1.7.5" + "@rspack/binding": "npm:1.7.7" "@rspack/lite-tapable": "npm:1.1.0" peerDependencies: "@swc/helpers": ">=0.5.1" peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/c17d93ef1e7e0728b74bf527150642d9bf072cabb63964ebd32c82da94d6d2f9eac7ff2cc13031bbd376c0c03710899f549e61d2bcfc0a6638a9f3bc8620b7ce + checksum: 10/f3f56aaeeda237d5862bbd76a0a2f012073f98d16bfebeeee0c961892f9f20434ebbe47e440cb482dc9971018b29b2d79fe2bec3ee99e1dadbcd46676b5155b7 languageName: node linkType: hard @@ -17799,6 +17906,27 @@ __metadata: languageName: node linkType: hard +"@rushstack/node-core-library@npm:5.20.1": + version: 5.20.1 + resolution: "@rushstack/node-core-library@npm:5.20.1" + dependencies: + ajv: "npm:~8.13.0" + ajv-draft-04: "npm:~1.0.0" + ajv-formats: "npm:~3.0.1" + fs-extra: "npm:~11.3.0" + import-lazy: "npm:~4.0.0" + jju: "npm:~1.4.0" + resolve: "npm:~1.22.1" + semver: "npm:~7.5.4" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/bd05a400fd96818a6382df7bc6a284adc78bbc8ae81c40760f2fee56a4124f0e56a38e007745bcf39c7449913e97f182860c1a344b56a31b8ba8445c21c6c012 + languageName: node + linkType: hard + "@rushstack/problem-matcher@npm:0.1.1": version: 0.1.1 resolution: "@rushstack/problem-matcher@npm:0.1.1" @@ -17811,13 +17939,25 @@ __metadata: languageName: node linkType: hard -"@rushstack/rig-package@npm:0.6.0": - version: 0.6.0 - resolution: "@rushstack/rig-package@npm:0.6.0" +"@rushstack/problem-matcher@npm:0.2.1": + version: 0.2.1 + resolution: "@rushstack/problem-matcher@npm:0.2.1" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/62fda91629577a2f57de19be357cd0990da145ff4933f4d2cd48f423cc03b92fca06dd8916dcbaf1d307a201c104847c77066d45d79fd3c323c4949f0c99bf44 + languageName: node + linkType: hard + +"@rushstack/rig-package@npm:0.7.1": + version: 0.7.1 + resolution: "@rushstack/rig-package@npm:0.7.1" dependencies: resolve: "npm:~1.22.1" strip-json-comments: "npm:~3.1.1" - checksum: 10/6ca5d6615365dfe4d78fdc52a1a145bec92bba79d8692db91d05c774b4ec4d9dc6c41b31949708d0312896b9c1c205a0f0eaa32f51ac7b1780415ac51c76af71 + checksum: 10/080a80e5c36b6861ee4a9a6e5ad9692cc3861cfb9edd0b02e9438aaaaa5a6e1b6f65275469d9f997696487841c7bb7daa69bba69c5e7301426056437bf544138 languageName: node linkType: hard @@ -17837,6 +17977,22 @@ __metadata: languageName: node linkType: hard +"@rushstack/terminal@npm:0.22.1": + version: 0.22.1 + resolution: "@rushstack/terminal@npm:0.22.1" + dependencies: + "@rushstack/node-core-library": "npm:5.20.1" + "@rushstack/problem-matcher": "npm:0.2.1" + supports-color: "npm:~8.1.1" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/fe4da212e11c60b8a6a2de9cb7658b03c510831d365c560eedf26a20fa85c62a45f1865cff99c2b252dcd773329fcd2347dd89e5e2efd5694d7746f0a8aec172 + languageName: node + linkType: hard + "@rushstack/ts-command-line@npm:5.1.5": version: 5.1.5 resolution: "@rushstack/ts-command-line@npm:5.1.5" @@ -17849,6 +18005,18 @@ __metadata: languageName: node linkType: hard +"@rushstack/ts-command-line@npm:5.3.1": + version: 5.3.1 + resolution: "@rushstack/ts-command-line@npm:5.3.1" + dependencies: + "@rushstack/terminal": "npm:0.22.1" + "@types/argparse": "npm:1.0.38" + argparse: "npm:~1.0.9" + string-argv: "npm:~0.3.1" + checksum: 10/51ca262eefbf07875f3e57fb402cba80e7ff36c14c0fc98c59af7be65407cafb4cbfe9b6738b549d91e687e40bb5720afb214efa1352a7a13c186241f92795f0 + languageName: node + linkType: hard + "@sagold/json-pointer@npm:^5.1.2": version: 5.1.2 resolution: "@sagold/json-pointer@npm:5.1.2" @@ -18126,13 +18294,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" @@ -18256,7 +18417,7 @@ __metadata: languageName: node linkType: hard -"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0, @slack/types@npm:^2.18.0": +"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0, @slack/types@npm:^2.20.0": version: 2.20.0 resolution: "@slack/types@npm:2.20.0" checksum: 10/c97c12c7de0ea37fdb0f47fefbe58f1ff0a4b1d993cc29a8a3bb7a370a41b6422abfe32a786ddf743cc6b17e464a0e5e1dc26dc2b05bb647a4febca51130444f @@ -18283,14 +18444,14 @@ __metadata: linkType: hard "@slack/web-api@npm:^7.5.0": - version: 7.13.0 - resolution: "@slack/web-api@npm:7.13.0" + version: 7.14.1 + resolution: "@slack/web-api@npm:7.14.1" dependencies: "@slack/logger": "npm:^4.0.0" - "@slack/types": "npm:^2.18.0" + "@slack/types": "npm:^2.20.0" "@types/node": "npm:>=18.0.0" "@types/retry": "npm:0.12.0" - axios: "npm:^1.11.0" + axios: "npm:^1.13.5" eventemitter3: "npm:^5.0.1" form-data: "npm:^4.0.4" is-electron: "npm:2.2.2" @@ -18298,11 +18459,11 @@ __metadata: p-queue: "npm:^6" p-retry: "npm:^4" retry: "npm:^0.13.1" - checksum: 10/9e254c372f1b4c3255acd317805f9dfabd38e90d0d8e96f2f79a35957d40d88eed5ba09ec59ae0838dbec32f99bd65ff2773c3528e44cd7aea273dd5e1fd3b82 + checksum: 10/50c61ff38dda7960eed9212f4754019bbbfab7396e49e9d75a336f5d953087ba9ba46fde133bf16d70516eadaffa7bc5c6b8d878752d30e1a8b124278e7560aa languageName: node linkType: hard -"@smithy/abort-controller@npm:^3.1.2, @smithy/abort-controller@npm:^3.1.9": +"@smithy/abort-controller@npm:^3.1.9": version: 3.1.9 resolution: "@smithy/abort-controller@npm:3.1.9" dependencies: @@ -18312,269 +18473,190 @@ __metadata: languageName: node linkType: hard -"@smithy/abort-controller@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/abort-controller@npm:4.2.5" +"@smithy/abort-controller@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/abort-controller@npm:4.2.11" dependencies: - "@smithy/types": "npm:^4.9.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/b0335823347ebbda10a03209ceeef83a711cd0ae8c1fb711e098402f107ea5d056ded24a17cad5a331955110e87377d716b6ed081e86c7b122d6fc6f5b74da67 + checksum: 10/9b97f1ec35b3f2fc4788359cdcd773abf7eb2953a233b207e16fc1f0c846da713396f9598f0461c3712d23da60d569587b7de1a20b7e28cb7f2ccf75fc10d09a languageName: node linkType: hard -"@smithy/chunked-blob-reader-native@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/chunked-blob-reader-native@npm:3.0.0" +"@smithy/chunked-blob-reader-native@npm:^4.2.3": + version: 4.2.3 + resolution: "@smithy/chunked-blob-reader-native@npm:4.2.3" dependencies: - "@smithy/util-base64": "npm:^3.0.0" + "@smithy/util-base64": "npm:^4.3.2" tslib: "npm:^2.6.2" - checksum: 10/424aa83f4fc081625a03ec6c64e74ae38c740c0b202d0b998f2bf341b935613491b39c7bf701790a0625219424340d5cfb042b701bfdff4c1cbedc57ee3f2500 + checksum: 10/f1348b053c1d3e5bade4ea567795f6b87cc257e2bac2d76c44e6ea562dc18923f83fdae35b9cf0b97aa2b1d133086000144d970cb445a07a94c5620dd1dcb22a languageName: node linkType: hard -"@smithy/chunked-blob-reader@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/chunked-blob-reader@npm:3.0.0" +"@smithy/chunked-blob-reader@npm:^5.2.2": + version: 5.2.2 + resolution: "@smithy/chunked-blob-reader@npm:5.2.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/1c7955ae693aa098dd0839d7e8f9e742ab963de4ededa92f201f1982552c35ba625c1b90cf761de81deddd5002ed10f081ad46f6e0a5150066cee8b00f3f6058 + checksum: 10/07f7eca6bed69b9ff1744d2f54acbeb05dfd534de535ee9d1ff75bc7c9c3725b32d91448990c5528bbacf893a96b969b683fdb473e871805fc38d8db40b1cb6f languageName: node linkType: hard -"@smithy/config-resolver@npm:^3.0.6": - version: 3.0.6 - resolution: "@smithy/config-resolver@npm:3.0.6" +"@smithy/config-resolver@npm:^4.4.10": + version: 4.4.10 + resolution: "@smithy/config-resolver@npm:4.4.10" dependencies: - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-config-provider": "npm:^3.0.0" - "@smithy/util-middleware": "npm:^3.0.4" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.2" + "@smithy/util-endpoints": "npm:^3.3.2" + "@smithy/util-middleware": "npm:^4.2.11" tslib: "npm:^2.6.2" - checksum: 10/424c2e81ce0507bb232ece4ea2e74872ae553e134f1612c638811dfb2937cbce15c846ac1193f5872854f0a638761bacc1bddc4232df10bfed3becae48d4c897 + checksum: 10/c2a5ad9e01c37b731fe183d73d284d13cd2c7596693d72544b1931673ebd4b1b4d8cac05d20ddcdffffb02805059815b3ed9613e7330484f9f64fb854cad2f4a languageName: node linkType: hard -"@smithy/config-resolver@npm:^4.4.3": - version: 4.4.3 - resolution: "@smithy/config-resolver@npm:4.4.3" +"@smithy/core@npm:^3.23.9": + version: 3.23.9 + resolution: "@smithy/core@npm:3.23.9" dependencies: - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-config-provider": "npm:^4.2.0" - "@smithy/util-endpoints": "npm:^3.2.5" - "@smithy/util-middleware": "npm:^4.2.5" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-stream": "npm:^4.5.17" + "@smithy/util-utf8": "npm:^4.2.2" + "@smithy/uuid": "npm:^1.1.2" tslib: "npm:^2.6.2" - checksum: 10/5a00a24d77afed5d820741fbf6f3f523bd4b36c3055cabe79221f0a2bcfa3baebe143997296f0d922707cf29341ec5c8aa4747ad0dba989a59f63f4f79ba5738 + checksum: 10/6169cfb1d498254e163611082af6c8b718b5f4133362e3135988824c3dd805b287656be608252fc1ad3005262a3a04434f36b10c65f5e4242d245562f718f6db languageName: node linkType: hard -"@smithy/core@npm:^2.4.1": - version: 2.4.1 - resolution: "@smithy/core@npm:2.4.1" +"@smithy/credential-provider-imds@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/credential-provider-imds@npm:4.2.11" dependencies: - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-retry": "npm:^3.0.16" - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-body-length-browser": "npm:^3.0.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-utf8": "npm:^3.0.0" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" tslib: "npm:^2.6.2" - checksum: 10/82665c86e8e7629d11c759debd95bc827b871469d796d23bbb0328971d48e16460847c39a8bdf920c5a9fd87d2680bbdeaf2688931d7810bddf5e029b2393292 + checksum: 10/1fc919e27b612fb445ce5063fe2c24f48b4ea403ec33116938bc30900dc20d9db648ea3b55310056e116d8c6e7f7bc9e2f2f4be114dc238b616523c252ce2907 languageName: node linkType: hard -"@smithy/core@npm:^3.18.7": - version: 3.18.7 - resolution: "@smithy/core@npm:3.18.7" - dependencies: - "@smithy/middleware-serde": "npm:^4.2.6" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-body-length-browser": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-stream": "npm:^4.5.6" - "@smithy/util-utf8": "npm:^4.2.0" - "@smithy/uuid": "npm:^1.1.0" - tslib: "npm:^2.6.2" - checksum: 10/eb03d40abc3dc8f9e543d3cfbe7066b9126b6b87a1ca7eeb805f1a0ed54dafbdabe4b5ce24eb84ce9f38bebf58ff1070041e7bb6436a1a179558ddebf2851a41 - languageName: node - linkType: hard - -"@smithy/credential-provider-imds@npm:^3.2.1": - version: 3.2.1 - resolution: "@smithy/credential-provider-imds@npm:3.2.1" - dependencies: - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - tslib: "npm:^2.6.2" - checksum: 10/8d413f24161549b6c7af55353844e2fb28f27836b12ce8ad884b8cd22a70aba0662a37215be021a2a00d962fd7a797ea9483a4df385d83b8b8c6f6716014a215 - languageName: node - linkType: hard - -"@smithy/credential-provider-imds@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/credential-provider-imds@npm:4.2.5" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - tslib: "npm:^2.6.2" - checksum: 10/45ce1b74f3259c073cfbbe8924b91db7e9061c98006476a7a457eb87f84b5687aba34df98513825537d3983e7763a8ce1fa851d24e47ff8731c3100f5defbb52 - languageName: node - linkType: hard - -"@smithy/eventstream-codec@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/eventstream-codec@npm:3.1.3" +"@smithy/eventstream-codec@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/eventstream-codec@npm:4.2.11" dependencies: "@aws-crypto/crc32": "npm:5.2.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-hex-encoding": "npm:^3.0.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-hex-encoding": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/f4901b03cd8c0122519de8af48bdd3b586d3adc6837d7ff214b13d8981052a8ab8c284f551a75cfba58ff3741aac167cfa19ce24f254d067b64287951dcd6993 + checksum: 10/d5055edb6592193156636829214ab13f9228e12e98009e79c6c86dcfe334c10e4e1961024477938be3c56a12f900e1d87ab49e462729a0e2cd9de49393791c0a languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^3.0.7": - version: 3.0.7 - resolution: "@smithy/eventstream-serde-browser@npm:3.0.7" +"@smithy/eventstream-serde-browser@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/eventstream-serde-browser@npm:4.2.11" dependencies: - "@smithy/eventstream-serde-universal": "npm:^3.0.6" - "@smithy/types": "npm:^3.4.0" + "@smithy/eventstream-serde-universal": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/203215491f2d1be688fbe3fe8c84d85c109cf703894707cff55e0f4720d1e7c81372f41f74d9a5da8733058311f201b73e80844b5d58bf03aab7c4a0c0967254 + checksum: 10/10a16bb000a0192598fccb7966de7340196de311c3234f573a829766b49c5218aa1d9b9c28fb31a4216b6d264038cadbddea00dc3913f47479b4467a10dd4f1c languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.4" +"@smithy/eventstream-serde-config-resolver@npm:^4.3.11": + version: 4.3.11 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.11" dependencies: - "@smithy/types": "npm:^3.4.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/073882d0b3b0b7f31261b5d900e147b5b193851affc46f89b1a737c6ab187e15de28a9521f84a39e649a6a6e8b8069cbc57f328e01d07a014c15da35f93bb38c + checksum: 10/27ddcb5af5e410af857adb99b0e0bd5aae05a9a8d9b26ca98a3ce54d6a67364df978014fbf6896fbc676b5ae6b63dd0106b60a566c040a2dcfe349b50ded65ca languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^3.0.6": - version: 3.0.6 - resolution: "@smithy/eventstream-serde-node@npm:3.0.6" +"@smithy/eventstream-serde-node@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/eventstream-serde-node@npm:4.2.11" dependencies: - "@smithy/eventstream-serde-universal": "npm:^3.0.6" - "@smithy/types": "npm:^3.4.0" + "@smithy/eventstream-serde-universal": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/635543e4312cc8cd95a5f1e79b728ea5e672aafba6d77f21700279ca9ba09a8cb30ea69eaa6cacf8176355e9db528b1bbb942566df92d2974ae927a951cf2321 + checksum: 10/348cc9b6d77ecaecd98d765c030699d451ec16440f009150e7b28b7bb92947127c68ef3fbee4b10ba538fa41cdbe73f77f5a9c14e7646dd97eabd4a53c850a81 languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^3.0.6": - version: 3.0.6 - resolution: "@smithy/eventstream-serde-universal@npm:3.0.6" +"@smithy/eventstream-serde-universal@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/eventstream-serde-universal@npm:4.2.11" dependencies: - "@smithy/eventstream-codec": "npm:^3.1.3" - "@smithy/types": "npm:^3.4.0" + "@smithy/eventstream-codec": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/073eab5d5d1d421afa881ca7cf184cf28f64bc7234478338e28fe47b261701b3f9c671cdbdb7a03f5c8e4e03ece007e0c317c46f82558e03a4931771a103dcdb + checksum: 10/8efb27a1932732fe7c5ec3ef3d3014f739f1c10eb015540f3e7e2cf2801f802a47c8dcb0ae0123a9629fc9030b86edaccabc9189101ac5badd5457158f2a21c1 languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^3.2.5": - version: 3.2.5 - resolution: "@smithy/fetch-http-handler@npm:3.2.5" +"@smithy/fetch-http-handler@npm:^5.3.13": + version: 5.3.13 + resolution: "@smithy/fetch-http-handler@npm:5.3.13" dependencies: - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/querystring-builder": "npm:^3.0.4" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-base64": "npm:^3.0.0" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/querystring-builder": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.2" tslib: "npm:^2.6.2" - checksum: 10/d9f6387699fa83342efe41dc5f5a3a496afcdcfc5064b379f4e45be7b3809d2a5a5b6d794dbc90494f4b647b18100b972d8fb47066231ae8bd1146457405e09c + checksum: 10/5e94cb268d4dd338bdbec418ea733c6e26fda7c8b384d2bebe5a21229ff68596bf027a7c7eef91ba41d085d810b7641a77f0b3cb3a0b34d25c5790a95c159cda languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^5.3.6": - version: 5.3.6 - resolution: "@smithy/fetch-http-handler@npm:5.3.6" +"@smithy/hash-blob-browser@npm:^4.2.12": + version: 4.2.12 + resolution: "@smithy/hash-blob-browser@npm:4.2.12" dependencies: - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/querystring-builder": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-base64": "npm:^4.3.0" + "@smithy/chunked-blob-reader": "npm:^5.2.2" + "@smithy/chunked-blob-reader-native": "npm:^4.2.3" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/270c8cd7541765ed33f6883d598b3bda1b211c3f8bd4130c5d3db27aa2e5972d6bc46cb9762a673cf62351bb9edf492fc8f863212a6b5abea57f9050738ab3f2 + checksum: 10/280fc7f0032a17f56c73aa655cdc407690def4136bb5122ca2e5550e901c26156a565dd131528b496e0f47d264c4d57ca66e74012f20e8a7d57775ae957193a7 languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/hash-blob-browser@npm:3.1.3" +"@smithy/hash-node@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/hash-node@npm:4.2.11" dependencies: - "@smithy/chunked-blob-reader": "npm:^3.0.0" - "@smithy/chunked-blob-reader-native": "npm:^3.0.0" - "@smithy/types": "npm:^3.4.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/26d1c76994d3c680ecb4e36f94a2f8c7d11054d685e200b420e7eda628415f58a293153b7dd1196e703fa0366c9fff3a74ff7024d95f2aeea5e72e9e4ab39521 + checksum: 10/a8b66b068d44ff404bbab79b02dff7294bcf1289a091a5e4a22a43f75ccaf0df01bc2eabc0dbcd69a7436d2eba3967e0a4e7e44c83df0e930a678744dd3c3590 languageName: node linkType: hard -"@smithy/hash-node@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/hash-node@npm:3.0.4" +"@smithy/hash-stream-node@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/hash-stream-node@npm:4.2.11" dependencies: - "@smithy/types": "npm:^3.4.0" - "@smithy/util-buffer-from": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^3.0.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/78d98c1d98e4f7595eb32368ffa15192314f9ee7874eccdef5e6f736a4afeb151f7e8d40d6d460705c58f32633bd9dc763ff9c9447813177c5e162df8e225abd + checksum: 10/b3caafc3f6557e5655328575d857c879fd0d31e0cc02b409ad507da42f6a2c5917fac24c2e00d1dd13566efdc53a7a91761b83fa4093f9052cbd062c7b235749 languageName: node linkType: hard -"@smithy/hash-node@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/hash-node@npm:4.2.5" +"@smithy/invalid-dependency@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/invalid-dependency@npm:4.2.11" dependencies: - "@smithy/types": "npm:^4.9.0" - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/cfdcb7459f54c0d7ecad300d24002c6043dfd12b4d3873048b6008a7566a3e8b53bdb141ddb485859ea155e2b4ae648bc21eec5e73ab73cc1aa215c32091c43c - languageName: node - linkType: hard - -"@smithy/hash-stream-node@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/hash-stream-node@npm:3.1.3" - dependencies: - "@smithy/types": "npm:^3.4.0" - "@smithy/util-utf8": "npm:^3.0.0" - tslib: "npm:^2.6.2" - checksum: 10/5c3faf2d6c846ceb914d5770823630e56f5b2c1a3fb2184d8a1a4668af23dcbc8337121ae3873bb86ba8f13f31434160a0d600fa1b60d9acdd150298959be901 - languageName: node - linkType: hard - -"@smithy/invalid-dependency@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/invalid-dependency@npm:3.0.4" - dependencies: - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/ad004c5fe4fe207714c39f3af7f9c0d240e9a782b0748eb08215317396d8da8b0329e4419e55acb4dcb072f200921af03697bc6f557109e0a27f0fa63a2eb78a - languageName: node - linkType: hard - -"@smithy/invalid-dependency@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/invalid-dependency@npm:4.2.5" - dependencies: - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/745b35246c473ec640798a4b3f3fa5fd24618778c4c2244c1917d929ca2e6bf21adc5ef90c8b279b286f917dfa31670a689609546a21a7383c62fec56402b231 + checksum: 10/2664b81a89c99182cbb19e25be843a05db822e8d24777e94e4235f73f8926da0bb1028ebd50f1df73ad4d8d8dc8fa1afda3684112571e3cb225b8599f7e286bc languageName: node linkType: hard @@ -18596,179 +18678,104 @@ __metadata: languageName: node linkType: hard -"@smithy/is-array-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/is-array-buffer@npm:4.2.0" +"@smithy/is-array-buffer@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/is-array-buffer@npm:4.2.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/fdc097ce6a8b241565e2d56460ec289730bcd734dcde17c23d1eaaa0996337f897217166276a3fd82491fe9fd17447aadf62e8d9056b3d2b9daf192b4b668af9 + checksum: 10/ebf9bac3daad0e1c3b201d41c4d2ab4be0c08c4c34604f87965b73cb052b1fd99133088f3b9837527f8fd6ed071b8684bb554ff381e5fdeacfc5907a66e4688b languageName: node linkType: hard -"@smithy/md5-js@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/md5-js@npm:3.0.4" +"@smithy/md5-js@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/md5-js@npm:4.2.11" dependencies: - "@smithy/types": "npm:^3.4.0" - "@smithy/util-utf8": "npm:^3.0.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/f84d4b37b713c6edd9e46f4e60a193fca4b6c882c4288683b334f0c3fc224abf884252e5fd9d55d04823192b1c0dca9c26f865599a8663397e51ce566c7df148 + checksum: 10/22eece7a9d58b155e5e735f8d91ba2ec0c2d4019d3eb96916cf9ec00f775b8c1d62a204f8e580a6f007cdb4046c501ec8869979c152480cce674e7a84e6dbef4 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^3.0.6": - version: 3.0.6 - resolution: "@smithy/middleware-content-length@npm:3.0.6" +"@smithy/middleware-content-length@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/middleware-content-length@npm:4.2.11" dependencies: - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/f9f3f3b6099ad5c5b697e5dca0016ff2f182ed0b66739c12c9cfa46f7c92761d9ffc8425097c929e094fd030a20f60ffbf6bd701d1062592dfcb0ba88a3fe53e + checksum: 10/35cbe15a0ece99c8f9f1f6a83efbd53dead37a534a1fede71c7a6a3cccb401458dafb03d07478690d67dfc2e7e444f00b86b2cff412d91924254e40ffa35c9b2 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/middleware-content-length@npm:4.2.5" +"@smithy/middleware-endpoint@npm:^4.4.23": + version: 4.4.23 + resolution: "@smithy/middleware-endpoint@npm:4.4.23" dependencies: - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" + "@smithy/core": "npm:^3.23.9" + "@smithy/middleware-serde": "npm:^4.2.12" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.11" + "@smithy/util-middleware": "npm:^4.2.11" tslib: "npm:^2.6.2" - checksum: 10/2c622812c4379f1957669ddd97a3daaf99302534e6f7a3cc2f220d65f56b84451a6d2e1d78dd79712b074ecdcf4bcbd9796b73af0c15df9245b192a747d410a7 + checksum: 10/9965bfcc274473cf2c640fe69f7a85fb5df2f665efeb9c84edd6a31f047208cafdd69a3dc2638d3f86f65cfd21550073ec6e4182cd5fa96db94b52afaa9b087e languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^3.1.1": - version: 3.1.1 - resolution: "@smithy/middleware-endpoint@npm:3.1.1" +"@smithy/middleware-retry@npm:^4.4.40": + version: 4.4.40 + resolution: "@smithy/middleware-retry@npm:4.4.40" dependencies: - "@smithy/middleware-serde": "npm:^3.0.4" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - "@smithy/url-parser": "npm:^3.0.4" - "@smithy/util-middleware": "npm:^3.0.4" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/service-error-classification": "npm:^4.2.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-retry": "npm:^4.2.11" + "@smithy/uuid": "npm:^1.1.2" tslib: "npm:^2.6.2" - checksum: 10/a4cc7321e1e38d99eb2a641f5a52c7c56e6bad6aecdceefb7200d97f26b0c146483d3b57c2e44330d730c445f416d26a6d2250ad7f39d6a98e9748bc8a358de7 + checksum: 10/052cc9d5352dd4d65db4cb8ec97eaa14df6511e3a35091b6d63f565e83307e5331b7395fcfd47a4521043a0af68128bf83972153c62cac5debc5a40be4b2d1e0 languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^4.3.14": - version: 4.3.14 - resolution: "@smithy/middleware-endpoint@npm:4.3.14" +"@smithy/middleware-serde@npm:^4.2.12": + version: 4.2.12 + resolution: "@smithy/middleware-serde@npm:4.2.12" dependencies: - "@smithy/core": "npm:^3.18.7" - "@smithy/middleware-serde": "npm:^4.2.6" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - "@smithy/url-parser": "npm:^4.2.5" - "@smithy/util-middleware": "npm:^4.2.5" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/2622afb558bdc89a459e6220a16406d614da9c89d002e4b9eff6255739a887fc7eabbd2af755446730e8eb17b1268421842de4abc053c7a0c8693fb423c1108e + checksum: 10/bdcc04ce3a551be4540e782fd7732e09979589b60debd897377f6a3b436f756d3cecc06c2ecbfbb7c80e4c2f2b894df1240ed5b74a41a010b4556d43d23757ad languageName: node linkType: hard -"@smithy/middleware-retry@npm:^3.0.16": - version: 3.0.16 - resolution: "@smithy/middleware-retry@npm:3.0.16" +"@smithy/middleware-stack@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/middleware-stack@npm:4.2.11" dependencies: - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/service-error-classification": "npm:^3.0.4" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-middleware": "npm:^3.0.4" - "@smithy/util-retry": "npm:^3.0.4" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - uuid: "npm:^9.0.1" - checksum: 10/90a3ec6dd79f200f74f9a025a6acf5ca4c87342826d88ccec7668addad1961032ba10a6018d6a16c43779dc2f296c6baf9dc00679d1113649381d41089a4decb + checksum: 10/4a44787456a5306f22db612d4d5ff42fb35d3ceda3a2b88d5884d740051f3e9f488dbf4edbc0f0b0f1571d51fd1a51d4e139a0f064c62a34a8861e31233ca660 languageName: node linkType: hard -"@smithy/middleware-retry@npm:^4.4.14": - version: 4.4.14 - resolution: "@smithy/middleware-retry@npm:4.4.14" +"@smithy/node-config-provider@npm:^4.3.11": + version: 4.3.11 + resolution: "@smithy/node-config-provider@npm:4.3.11" dependencies: - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/service-error-classification": "npm:^4.2.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-retry": "npm:^4.2.5" - "@smithy/uuid": "npm:^1.1.0" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/4e5eee34c97ca971914b684cd0d5eeeca5213893f6c7b29b029d4b9a516458eda52bff7d6e2470de8d7e89c47cabb7bbaa0101e5d1f58740f1078b26b9abbd4d + checksum: 10/a1b1840a8f52eb91b5b74dc87925de79bdd6d0b16937a5a318d1d1081ad01aac00a6822cad2df1bf6485da55fab3808027dabb248854d48f3cde6a7a04c8ac07 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/middleware-serde@npm:3.0.4" - dependencies: - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/a4d3dd79b6cc314245bfcb0e649e5869cabf6cfb8e9685ae797919ecaeb123eb0335c054c9278dba1304fe4a0cd06e71d6a77e0442f62530bfabd5df79c819da - languageName: node - linkType: hard - -"@smithy/middleware-serde@npm:^4.2.6": - version: 4.2.6 - resolution: "@smithy/middleware-serde@npm:4.2.6" - dependencies: - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/87aa0c6bf9743d003fed4f1d544a69610dae0005a58241b4fa8aab1c48746f104bbbb549df7f89b7040134a2af645ebecc375b3abde26c910d5e4e4c26c28b88 - languageName: node - linkType: hard - -"@smithy/middleware-stack@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/middleware-stack@npm:3.0.4" - dependencies: - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/63bf03bd512367ff781e3a220a5126cb3784e982891e1dcb00cf975da6801d454ffe9d244ac67f6db2ee2f34146089d113bc9ae25f190c12a318d121ccdd03fb - languageName: node - linkType: hard - -"@smithy/middleware-stack@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/middleware-stack@npm:4.2.5" - dependencies: - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/146647dabe0bc20415b82a0862b1d65ceb4f98167fff19f7002d6a8ea933527c19b5081cd4fdc4532f803e8085356e987d72c659501dccbdd8c8056a7d394ce7 - languageName: node - linkType: hard - -"@smithy/node-config-provider@npm:^3.1.5": - version: 3.1.5 - resolution: "@smithy/node-config-provider@npm:3.1.5" - dependencies: - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/shared-ini-file-loader": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/1efa20757cb0d21eafc377d1820c2b644ef3a45ace4c2012e299ff4f9cb1f390c3ab72de5827d50302dccc5e6f1ef741778befcdb91ef5c319de9b1888cd1b96 - languageName: node - linkType: hard - -"@smithy/node-config-provider@npm:^4.3.5": - version: 4.3.5 - resolution: "@smithy/node-config-provider@npm:4.3.5" - dependencies: - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/shared-ini-file-loader": "npm:^4.4.0" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/d0044a205b765be9648c44654e189804bf1cf83aed002fd5ff1cb7abbc9ed4b29886bb9b9c87fe440b8c319b3e1e2aecafec5aa147569d5921e35b1e2692f010 - languageName: node - linkType: hard - -"@smithy/node-http-handler@npm:^3.0.0, @smithy/node-http-handler@npm:^3.2.0": +"@smithy/node-http-handler@npm:^3.0.0": version: 3.3.3 resolution: "@smithy/node-http-handler@npm:3.3.3" dependencies: @@ -18781,40 +18788,30 @@ __metadata: languageName: node linkType: hard -"@smithy/node-http-handler@npm:^4.4.5": - version: 4.4.5 - resolution: "@smithy/node-http-handler@npm:4.4.5" +"@smithy/node-http-handler@npm:^4.4.14": + version: 4.4.14 + resolution: "@smithy/node-http-handler@npm:4.4.14" dependencies: - "@smithy/abort-controller": "npm:^4.2.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/querystring-builder": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" + "@smithy/abort-controller": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/querystring-builder": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/0748c69d581c01144360c81aa3f0eeb619c3e473540846a59949031b25bbd4a83e82beabd801c4bc9b4774f53a2e1ab00a5d3afb847f7a245788630b7c3b307a + checksum: 10/88e6c694fa9c0985dcd9c770a928e39392a9f1f41903e07e87fb7f6e6acc577d7013459cf55a4bf93cc17077503e3c6906552dd8877734cfedee3424505ce502 languageName: node linkType: hard -"@smithy/property-provider@npm:^3.1.4": - version: 3.1.4 - resolution: "@smithy/property-provider@npm:3.1.4" +"@smithy/property-provider@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/property-provider@npm:4.2.11" dependencies: - "@smithy/types": "npm:^3.4.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/d1c1597be13f4ea43d61117e388a5ea574163d888bf55f1df182c978fa2d92231ec0ff98b4a6c338aa39b9f71e379e1ddb2567b3f9979c633c957165426c02c2 + checksum: 10/a78b3a55c0ede47109245982bcec832ebf9d86891f86b3cad2d52702e57313873cffd7611859a47b51db7137cacc349ce7a836b8d7c1b1d185936dc3fc6f43d9 languageName: node linkType: hard -"@smithy/property-provider@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/property-provider@npm:4.2.5" - dependencies: - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/fc7b1b26f4a0ae3bdf3e742607cc1fa4c81dbf166bdd908a528e2613dd76923722e927be66edd4ddcc203ca2c33a32c0d0088dd89132ef4feb265d7fa43b44da - languageName: node - linkType: hard - -"@smithy/protocol-http@npm:^4.1.1, @smithy/protocol-http@npm:^4.1.8": +"@smithy/protocol-http@npm:^4.1.8": version: 4.1.8 resolution: "@smithy/protocol-http@npm:4.1.8" dependencies: @@ -18824,17 +18821,17 @@ __metadata: languageName: node linkType: hard -"@smithy/protocol-http@npm:^5.3.5": - version: 5.3.5 - resolution: "@smithy/protocol-http@npm:5.3.5" +"@smithy/protocol-http@npm:^5.3.11": + version: 5.3.11 + resolution: "@smithy/protocol-http@npm:5.3.11" dependencies: - "@smithy/types": "npm:^4.9.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/444c40f8a0cdd2b7d73a0d48527903f5a76346e93b9c1a2d9fd2b45a925259eb2afce2c3ce384a0f52eda8c82ffcd5ef137ac52580fd99f7b20c93d54191baf2 + checksum: 10/52bb86ccde22344b8b7136fc000774a4924896e0c6f4ad036e2b4bdfa84d5310d011b69b0d56ff65652a6c982c89d6b20ab79e65bc27c1758af72ac070ab5a9b languageName: node linkType: hard -"@smithy/querystring-builder@npm:^3.0.11, @smithy/querystring-builder@npm:^3.0.4": +"@smithy/querystring-builder@npm:^3.0.11": version: 3.0.11 resolution: "@smithy/querystring-builder@npm:3.0.11" dependencies: @@ -18845,76 +18842,47 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-builder@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/querystring-builder@npm:4.2.5" +"@smithy/querystring-builder@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/querystring-builder@npm:4.2.11" dependencies: - "@smithy/types": "npm:^4.9.0" - "@smithy/util-uri-escape": "npm:^4.2.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-uri-escape": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/34385aa700bd4476ac40901b3be11c389c898a54a84c6a109794493d9bd285d0e6fe24c8d4cc696be04e0cd0b105985cc244fe36a00a99db04ec6f81e11cae69 + checksum: 10/9fdbd32249aadc444b7d5167bf8e806af375bb98f39597c0efa8ba11d72e812ced186f5bca9bf9aaf2b6679fa970a0e9b584621e14bc927a5c045ea1d373ca50 languageName: node linkType: hard -"@smithy/querystring-parser@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/querystring-parser@npm:3.0.4" +"@smithy/querystring-parser@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/querystring-parser@npm:4.2.11" dependencies: - "@smithy/types": "npm:^3.4.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/a985e95590ea3acc84f4123aa951bd6fdcc4b3cb83ef179d906635f82589b025cdde6f73c6c3417b18a569c6aee82ff9345d34e3b3d976f71ea5d9132033adea + checksum: 10/3e0f68b957d2012648d861ad20da39222cc39f04bdfea43041c8ca05e70ac5a35fefe25c43c7c6aacdbaea80743a085267b98765d75a408aed2bc531fad33529 languageName: node linkType: hard -"@smithy/querystring-parser@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/querystring-parser@npm:4.2.5" +"@smithy/service-error-classification@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/service-error-classification@npm:4.2.11" dependencies: - "@smithy/types": "npm:^4.9.0" + "@smithy/types": "npm:^4.13.0" + checksum: 10/db128e423dfdfa1ff60911a19d659201da52c5eebd26fb9269d48c11af29655884dddcef26d4551b1714cd55afd7474a19d59794c9bb9baca151ca418cdab787 + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^4.4.6": + version: 4.4.6 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.6" + dependencies: + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/4d3c12436409be000c865fe46fe792454d518b9939971d5f8f19d73b7e04ce26654c13555a81eaf72092bc92cdb0dddc762891f1e73785c0827933b95b81b42a + checksum: 10/086bf680702608577175b7242e2560ff3d4a9cd697ebf8f297c28b958f833bc7c672107e7f7418aba6f193b6c83fd56c7ed296f88a1e99a367fb71437c766ceb languageName: node linkType: hard -"@smithy/service-error-classification@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/service-error-classification@npm:3.0.4" - dependencies: - "@smithy/types": "npm:^3.4.0" - checksum: 10/a06600871da110279b7e44901b395e95965391de5456ac747b972bd0330484270946c0eae796f1ba60b6ebe8c5e4a5447f1ccd5d6793b27b45dbc12d1776fad4 - languageName: node - linkType: hard - -"@smithy/service-error-classification@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/service-error-classification@npm:4.2.5" - dependencies: - "@smithy/types": "npm:^4.9.0" - checksum: 10/4466f742bbc960411deee6424d15525ed4cc8cc024c82c8023f380213c3e17d3eee57db91428df3b1b55281a463b8178d5f69100de48aaf0fd0e39ff81017bff - languageName: node - linkType: hard - -"@smithy/shared-ini-file-loader@npm:^3.1.5": - version: 3.1.5 - resolution: "@smithy/shared-ini-file-loader@npm:3.1.5" - dependencies: - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/722e375db736195c12680558a395129affbcf486219da80684363b7035bdef6394399881696dd4645ce7af28197a50aed66a26bb2a526195fab419a87eb324cb - languageName: node - linkType: hard - -"@smithy/shared-ini-file-loader@npm:^4.4.0": - version: 4.4.0 - resolution: "@smithy/shared-ini-file-loader@npm:4.4.0" - dependencies: - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/09dd2fc84ce8c356995802388a3fb2d954c7a356e7d62e059ed969e28e79d8b07d84109bca81e2b2447d7f1a57f7e800bd14138552cba163253f30f5345ff524 - languageName: node - linkType: hard - -"@smithy/signature-v4@npm:^4.1.0, @smithy/signature-v4@npm:^4.1.1": +"@smithy/signature-v4@npm:^4.1.0": version: 4.2.4 resolution: "@smithy/signature-v4@npm:4.2.4" dependencies: @@ -18930,48 +18898,34 @@ __metadata: languageName: node linkType: hard -"@smithy/signature-v4@npm:^5.3.5": - version: 5.3.5 - resolution: "@smithy/signature-v4@npm:5.3.5" +"@smithy/signature-v4@npm:^5.3.11": + version: 5.3.11 + resolution: "@smithy/signature-v4@npm:5.3.11" dependencies: - "@smithy/is-array-buffer": "npm:^4.2.0" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-hex-encoding": "npm:^4.2.0" - "@smithy/util-middleware": "npm:^4.2.5" - "@smithy/util-uri-escape": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" + "@smithy/is-array-buffer": "npm:^4.2.2" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.11" + "@smithy/util-uri-escape": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/aa615f431d436c6ccdee159e9fea15b56e7754bf713f9c0d6e0d75a915abe8a938a0b3eb36306c2ae9cd0713198ea38410f05f60bcf58ee68bbf22ae9639ce39 + checksum: 10/c4ba4bfdf4cb9d5ddb1d5c6f2f5ff5a8b1221e7e389586b9b14119a0e9a7368607eff6c1708de8c32e6b2789cec4ade0020e3447e14f4a44f4c6e042d1f743f9 languageName: node linkType: hard -"@smithy/smithy-client@npm:^3.3.0": - version: 3.3.0 - resolution: "@smithy/smithy-client@npm:3.3.0" +"@smithy/smithy-client@npm:^4.12.3": + version: 4.12.3 + resolution: "@smithy/smithy-client@npm:4.12.3" dependencies: - "@smithy/middleware-endpoint": "npm:^3.1.1" - "@smithy/middleware-stack": "npm:^3.0.4" - "@smithy/protocol-http": "npm:^4.1.1" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-stream": "npm:^3.1.4" + "@smithy/core": "npm:^3.23.9" + "@smithy/middleware-endpoint": "npm:^4.4.23" + "@smithy/middleware-stack": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.11" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-stream": "npm:^4.5.17" tslib: "npm:^2.6.2" - checksum: 10/b060a7ae03ff9fc98a7478114709466a4d0813eda3d77e98567733f42be376d4935650abb34d6a9fe41610f5498096c06fa0677bdbe24f2d455c733f46ed48e6 - languageName: node - linkType: hard - -"@smithy/smithy-client@npm:^4.9.10": - version: 4.9.10 - resolution: "@smithy/smithy-client@npm:4.9.10" - dependencies: - "@smithy/core": "npm:^3.18.7" - "@smithy/middleware-endpoint": "npm:^4.3.14" - "@smithy/middleware-stack": "npm:^4.2.5" - "@smithy/protocol-http": "npm:^5.3.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-stream": "npm:^4.5.6" - tslib: "npm:^2.6.2" - checksum: 10/c4e593f1e91da83c1e144d1c2e85ae88327ac99d1b11b7f1dfb6b76a63029ae3f73396893834c6beae017be0308ecab5c4c1f5449ab6aff11f55cebaa911d256 + checksum: 10/e302202a2f096bc79265cf21557b211f37df2ae272f38a743a47e7c710e0b91609bbb25e161051589fdf5bafd274eb0105a0ec6085f5f3598d3fc80850f045ee languageName: node linkType: hard @@ -18984,7 +18938,7 @@ __metadata: languageName: node linkType: hard -"@smithy/types@npm:^3.4.0, @smithy/types@npm:^3.7.2": +"@smithy/types@npm:^3.7.2": version: 3.7.2 resolution: "@smithy/types@npm:3.7.2" dependencies: @@ -18993,92 +18947,52 @@ __metadata: languageName: node linkType: hard -"@smithy/types@npm:^4.9.0": - version: 4.9.0 - resolution: "@smithy/types@npm:4.9.0" +"@smithy/types@npm:^4.13.0": + version: 4.13.0 + resolution: "@smithy/types@npm:4.13.0" dependencies: tslib: "npm:^2.6.2" - checksum: 10/b966ddb05487ee634555d248c83838012c4d1cbbd17c28799743dea65ef3c597695a1a47c9cb466e1ce5dbdd1e4bb23f0b4cdfb3595e12fd892a31c275a04ea5 + checksum: 10/abcc033be2ee9500f4befd988879a5dcedbe460e445207095a17eb974f9f07ec31cbf0b27754b704a9ca9a4fc17891948026438d34215b84d539e4bdaf5ac7b6 languageName: node linkType: hard -"@smithy/url-parser@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/url-parser@npm:3.0.4" +"@smithy/url-parser@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/url-parser@npm:4.2.11" dependencies: - "@smithy/querystring-parser": "npm:^3.0.4" - "@smithy/types": "npm:^3.4.0" + "@smithy/querystring-parser": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/0b582094db501fb9f285fbc61503ac32191557ef6467f75e4019b1ea3ebaa2ceaa6bebd2b7db54b68ad244a67c29d3ea1262006dd252bb78acc95126b1035ba5 + checksum: 10/bcbb7b9da41603c54f8b8b459e2b0d82bb98832906dd5577ba2b5b388b2570f583e608ebff2ae32b4f5f88c25cce763bd48b9e378c101b06aa5275ef4ae3a745 languageName: node linkType: hard -"@smithy/url-parser@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/url-parser@npm:4.2.5" +"@smithy/util-base64@npm:^4.3.2": + version: 4.3.2 + resolution: "@smithy/util-base64@npm:4.3.2" dependencies: - "@smithy/querystring-parser": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/f2649f43f9f569f90c3e68023e938f80fd43fbfce2c454162507ba7d39497f2f89e9ade2369ddc07d9aa4b7ebd7dc80b22922de8488bef04f447467949f62fc3 + checksum: 10/9246e1874c2e96c2059e97c88e039bf40abb8b0d6fb229e1aca9df896371c44a9dc951c99d2febbd8267033920cef4180852e6655f3de8c4743d9a9bbf6d96b4 languageName: node linkType: hard -"@smithy/util-base64@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/util-base64@npm:3.0.0" +"@smithy/util-body-length-browser@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-body-length-browser@npm:4.2.2" dependencies: - "@smithy/util-buffer-from": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^3.0.0" tslib: "npm:^2.6.2" - checksum: 10/3c883d63a33e1cfeebf7f846f167dfc54c832d1f5514d014fbfff06de3aecd5919f01637fc93668dca8a1029752f3a6fab0a94f455dcb7c88f1d472bde294eef + checksum: 10/c3058710fddecee22d4bc03839310c13783a7593a0a1808998998c465f6408012b5ce34bbec379fff84fb56f4017747d7817c4a7f050b45834b2aada27a2e7a7 languageName: node linkType: hard -"@smithy/util-base64@npm:^4.3.0": - version: 4.3.0 - resolution: "@smithy/util-base64@npm:4.3.0" - dependencies: - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/87065ca13e3745858e0bb0ab6374433b258c378ee2a5ef865b74f6a4208c56db7db2b9ee5f888e021de0107fae49e9957662c4c6847fe10529e2f6cc882426b4 - languageName: node - linkType: hard - -"@smithy/util-body-length-browser@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/util-body-length-browser@npm:3.0.0" +"@smithy/util-body-length-node@npm:^4.2.3": + version: 4.2.3 + resolution: "@smithy/util-body-length-node@npm:4.2.3" dependencies: tslib: "npm:^2.6.2" - checksum: 10/a0ab6a6d414a62d18e57f3581769379a54bb1fd93606c69bc8d96a3566fdecb8db7b57da9446568d03eef9f004f2a89d7e94bdda79ef280f28b19a71803c0309 - languageName: node - linkType: hard - -"@smithy/util-body-length-browser@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-body-length-browser@npm:4.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/deeb689b52652651c11530a324e07725805533899215ad1f93c5e9a14931443e22b313491a3c2a6d7f61d6dd1e84f9154d0d32de62bf61e0bd8e6ab7bf5f81ed - languageName: node - linkType: hard - -"@smithy/util-body-length-node@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/util-body-length-node@npm:3.0.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/aabac66d7111612fd375d67150f8787c5cdc828f7e6acb40ef0b18b4c354e64e0ef2e4b8da2d7f01e8abe931ff2ef8109a408164ce7e5662fd41b470c462f1e4 - languageName: node - linkType: hard - -"@smithy/util-body-length-node@npm:^4.2.1": - version: 4.2.1 - resolution: "@smithy/util-body-length-node@npm:4.2.1" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10/efb1333d35120124ec0c751b7b7d5657eb9ad6d0bf6171ff61fde2504639883d36e9562613c70eca623b726193b22601c8ff60e40a8156102d4c5b12fae222f8 + checksum: 10/ad271366f72505f66ac82b87f0964211c79f40cb5bebeceeb23d74613ce33f1e77a9e81b03b48aced35c76ddd7184103886ce13dde96c8ee6e05dec59239798b languageName: node linkType: hard @@ -19102,108 +19016,60 @@ __metadata: languageName: node linkType: hard -"@smithy/util-buffer-from@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-buffer-from@npm:4.2.0" +"@smithy/util-buffer-from@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-buffer-from@npm:4.2.2" dependencies: - "@smithy/is-array-buffer": "npm:^4.2.0" + "@smithy/is-array-buffer": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/6a81e658554d7123fe089426a840b5e691aee4aa4f0d72b79af19dcf57ccb212dca518acb447714792d48c2dc99bda5e0e823dab05e450ee2393146706d476f9 + checksum: 10/111148eb7fb8c2913c0f09ca9991a409c69b2df643aa73378e64e14404ce040f67c716f7b4f55b76c0640f4357b649b9eb6a7f1539d7b37a2f0a7e0c3ba7062d languageName: node linkType: hard -"@smithy/util-config-provider@npm:^3.0.0": - version: 3.0.0 - resolution: "@smithy/util-config-provider@npm:3.0.0" +"@smithy/util-config-provider@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-config-provider@npm:4.2.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/614c321c5a5a220d7d72d36359c41b4e390566719f7e01cefebffbe7034aae78b4533b27ab2030f93186c5f22893ddf056a3a2376a077d70ce89275f31e1ac46 + checksum: 10/ff3989fc07b5162674ccce6aacf7c74dbaea9e7a731c05399a80ed758a67adf83e87d47431a69aa2b1c325497670ff7d1390d9441e3c0c2cea66e830f61f965a languageName: node linkType: hard -"@smithy/util-config-provider@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-config-provider@npm:4.2.0" +"@smithy/util-defaults-mode-browser@npm:^4.3.39": + version: 4.3.39 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.39" dependencies: + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/d65f36401c7a085660cf201a1b317d271e390258b619179fff88248c2db64fc35e6c62fe055f1e55be8935b06eb600379824dabf634fb26d528f54fe60c9d77b + checksum: 10/a977199c6e2f336f8de145764754ffb0b05e731a80b128631c4643820cd175a87d1b612e44c8bdda71e37f3603ff2057530df46f5a6ebfa0647fdf4da5a2ef91 languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^3.0.16": - version: 3.0.16 - resolution: "@smithy/util-defaults-mode-browser@npm:3.0.16" +"@smithy/util-defaults-mode-node@npm:^4.2.42": + version: 4.2.42 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.42" dependencies: - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - bowser: "npm:^2.11.0" + "@smithy/config-resolver": "npm:^4.4.10" + "@smithy/credential-provider-imds": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/property-provider": "npm:^4.2.11" + "@smithy/smithy-client": "npm:^4.12.3" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/5a4a3a7b9df20e84ce9f7555860ebabd2afdb48614a4ef15c75ea1895f6567baa0dbe5670cce3f05d62a4fd080fad650623915e73ee90b5ab29afe4c4c55ed15 + checksum: 10/f2d578709627bfa1d99705aa9e1489ce60f25fa6c5a37e74152ff94e7582ed4963d44d6436c4e6692dbfa9a3c1b790780bb00493cd19c8637261cd8bbbc1e039 languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^4.3.13": - version: 4.3.13 - resolution: "@smithy/util-defaults-mode-browser@npm:4.3.13" +"@smithy/util-endpoints@npm:^3.3.2": + version: 3.3.2 + resolution: "@smithy/util-endpoints@npm:3.3.2" dependencies: - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" + "@smithy/node-config-provider": "npm:^4.3.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/a42116ddd4638e12137d520ace2d2acc32708a98d79de06a91ef949d2163f7246bfd400faad8a1ab5f0f5a5a55d4d73b9b7d70edd2b36d83fdb9e71d1b06e56e - languageName: node - linkType: hard - -"@smithy/util-defaults-mode-node@npm:^3.0.16": - version: 3.0.16 - resolution: "@smithy/util-defaults-mode-node@npm:3.0.16" - dependencies: - "@smithy/config-resolver": "npm:^3.0.6" - "@smithy/credential-provider-imds": "npm:^3.2.1" - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/property-provider": "npm:^3.1.4" - "@smithy/smithy-client": "npm:^3.3.0" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/4b2a8b982cc694c6cd43ed2c721d042093e3133aba4cf6c7bae92608905c5e764fda73730f56456d39cc2fe4b56a4e9eb2fb221435db9f9ff821167fff79628a - languageName: node - linkType: hard - -"@smithy/util-defaults-mode-node@npm:^4.2.16": - version: 4.2.16 - resolution: "@smithy/util-defaults-mode-node@npm:4.2.16" - dependencies: - "@smithy/config-resolver": "npm:^4.4.3" - "@smithy/credential-provider-imds": "npm:^4.2.5" - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/property-provider": "npm:^4.2.5" - "@smithy/smithy-client": "npm:^4.9.10" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/9c466504abf0b8ef37bf4a72e85b1d164dcd9e8426cb30e9bbff46b0fdb536cfab21c8f5650e4500bd9a120749b462c345b681fef72d61ef7331a835c4b007ff - languageName: node - linkType: hard - -"@smithy/util-endpoints@npm:^2.1.0": - version: 2.1.0 - resolution: "@smithy/util-endpoints@npm:2.1.0" - dependencies: - "@smithy/node-config-provider": "npm:^3.1.5" - "@smithy/types": "npm:^3.4.0" - tslib: "npm:^2.6.2" - checksum: 10/813d5f60132315912c6170f25021db69c8f7ade602faa3be0a6d96d44fb87a399f5a21408f5435dbd9fe55cde8a5d9f26bf842eb1f9ef72c03c4febf30b8e33e - languageName: node - linkType: hard - -"@smithy/util-endpoints@npm:^3.2.5": - version: 3.2.5 - resolution: "@smithy/util-endpoints@npm:3.2.5" - dependencies: - "@smithy/node-config-provider": "npm:^4.3.5" - "@smithy/types": "npm:^4.9.0" - tslib: "npm:^2.6.2" - checksum: 10/3a91786650d007d6dd529107960771bb81d5fb5aef9a2189edfab6ecbfdbd673d72c8d6c1fe65c7814d34244bf448bccd7f64796646d41281b7ba18d37bdd967 + checksum: 10/dae5354ff924bf03ffc94e583ecc83b99f8114fbca0940df64bf5e53f1d60bfd9c661ea4dc79037be164b14b501b13af90754e9adbbca290f6ba8b619526a913 languageName: node linkType: hard @@ -19216,16 +19082,16 @@ __metadata: languageName: node linkType: hard -"@smithy/util-hex-encoding@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-hex-encoding@npm:4.2.0" +"@smithy/util-hex-encoding@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-hex-encoding@npm:4.2.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/478773d73690e39167b67481116c4fd47cecfc97c3a935d88db9271fb0718627bec1cbc143efbf0cd49d1ac417bde7e76aa74139ea07e365b51e66797f63a45d + checksum: 10/2b1dfed0fcbe13c9a449b06adf805814fb3ec5d0d614704bfb250875cec7cf19f5a77a81013c91b81f45b7193038268f92d59de339192d578c9ef77a1b51c4d9 languageName: node linkType: hard -"@smithy/util-middleware@npm:^3.0.11, @smithy/util-middleware@npm:^3.0.4": +"@smithy/util-middleware@npm:^3.0.11": version: 3.0.11 resolution: "@smithy/util-middleware@npm:3.0.11" dependencies: @@ -19235,67 +19101,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/util-middleware@npm:4.2.5" +"@smithy/util-middleware@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/util-middleware@npm:4.2.11" dependencies: - "@smithy/types": "npm:^4.9.0" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/845feb06378be04902ae371daf94bd90a739dde0cb3780f3b438c70d8af7d69a65cb231946e40e65a8ead1488b716509a197a9716d4faa200dd32a385ea8387c + checksum: 10/85043b9ccd7ec114d862b293208acf5406dad9339e7ac205d4c8445e50fa321f2aa72b9cda1928df043fc4069b277344e416461420fc413c4c6e27143ee06876 languageName: node linkType: hard -"@smithy/util-retry@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/util-retry@npm:3.0.4" +"@smithy/util-retry@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/util-retry@npm:4.2.11" dependencies: - "@smithy/service-error-classification": "npm:^3.0.4" - "@smithy/types": "npm:^3.4.0" + "@smithy/service-error-classification": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/9d8e4ee8a3c71e3f7cd799a03fc8d2cb69e294d475f798d3fd9db85f64135ff545d333d9ca89fa7b621dfa17fa6f91f7c2d90a889089e0417484d821a627e225 + checksum: 10/279424cf24212a66cee16fcb181793958dd468783076d4fc4c36b80fe2ec9b0a4b973f01bb107d3f9d57737ad4daee0c2d2c82b9403d943ee69135f04c58aa77 languageName: node linkType: hard -"@smithy/util-retry@npm:^4.2.5": - version: 4.2.5 - resolution: "@smithy/util-retry@npm:4.2.5" +"@smithy/util-stream@npm:^4.5.17": + version: 4.5.17 + resolution: "@smithy/util-stream@npm:4.5.17" dependencies: - "@smithy/service-error-classification": "npm:^4.2.5" - "@smithy/types": "npm:^4.9.0" + "@smithy/fetch-http-handler": "npm:^5.3.13" + "@smithy/node-http-handler": "npm:^4.4.14" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/e2715477f2021327e6cc3a796955d74659cbe18e264969cb1188964b82b19fa03eca82d9fe51dc3ca5839d9c654c0357dc670c85543402150536281ef3c14402 - languageName: node - linkType: hard - -"@smithy/util-stream@npm:^3.1.4": - version: 3.1.4 - resolution: "@smithy/util-stream@npm:3.1.4" - dependencies: - "@smithy/fetch-http-handler": "npm:^3.2.5" - "@smithy/node-http-handler": "npm:^3.2.0" - "@smithy/types": "npm:^3.4.0" - "@smithy/util-base64": "npm:^3.0.0" - "@smithy/util-buffer-from": "npm:^3.0.0" - "@smithy/util-hex-encoding": "npm:^3.0.0" - "@smithy/util-utf8": "npm:^3.0.0" - tslib: "npm:^2.6.2" - checksum: 10/37970467674b504aef7cd1a241bbe4227c27449647567d8f4b465cf2868b4d4c0b63fe29826f9b8f17a9440572f53cc6a1993e5c8872f5409115b073f4b3cd8f - languageName: node - linkType: hard - -"@smithy/util-stream@npm:^4.5.6": - version: 4.5.6 - resolution: "@smithy/util-stream@npm:4.5.6" - dependencies: - "@smithy/fetch-http-handler": "npm:^5.3.6" - "@smithy/node-http-handler": "npm:^4.4.5" - "@smithy/types": "npm:^4.9.0" - "@smithy/util-base64": "npm:^4.3.0" - "@smithy/util-buffer-from": "npm:^4.2.0" - "@smithy/util-hex-encoding": "npm:^4.2.0" - "@smithy/util-utf8": "npm:^4.2.0" - tslib: "npm:^2.6.2" - checksum: 10/6406467f64a39eae77df30ea6e45c13988aff86b5de5b3f4f91929fa8eb6cd786cbda24832e489ecaa559fb9d1514d6def86765c0f548e966cbdaa98cb3a8d77 + checksum: 10/5da4c209f3d16900352882f465cb5e92b5a48f3d1aa507006c62a3eccc32538daa44ca364e374bcf1a4288f114210a876d8f7e995285e54d458658da9d6e5a1c languageName: node linkType: hard @@ -19308,12 +19147,12 @@ __metadata: languageName: node linkType: hard -"@smithy/util-uri-escape@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-uri-escape@npm:4.2.0" +"@smithy/util-uri-escape@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-uri-escape@npm:4.2.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/a838a3afe557d7087d4500735c79d5da72e0cd5a08f95d1a1c450ba29d9cd85c950228eedbd9b2494156f4eb8658afb0a9a5bd2df3fc4f297faed886c396242b + checksum: 10/2b649e431a92c89e4fb7cc817e7bfcbe547d07f725c401445ea81aaea37e4ede383e1e2b9c3f0dfb228dee597166e95805a2f3e57fa6ae1b5341abc48a397935 languageName: node linkType: hard @@ -19337,39 +19176,39 @@ __metadata: languageName: node linkType: hard -"@smithy/util-utf8@npm:^4.2.0": - version: 4.2.0 - resolution: "@smithy/util-utf8@npm:4.2.0" +"@smithy/util-utf8@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-utf8@npm:4.2.2" dependencies: - "@smithy/util-buffer-from": "npm:^4.2.0" + "@smithy/util-buffer-from": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10/d49f58fc6681255eecc3dee39c657b80ef8a4c5617e361bdaf6aaa22f02e378622376153cafc9f0655fb80162e88fc98bbf459f8dd5ba6d7c4b9a59e6eaa05f8 + checksum: 10/4dd23ac07cab78279a9f4f250b43df69d3303458b741e38c447ba7e92f7c6b1651076479023687b9eb3996aa3269ee1a0d363a1c320d15e78247ca9ea74aca58 languageName: node linkType: hard -"@smithy/util-waiter@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/util-waiter@npm:3.1.3" +"@smithy/util-waiter@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/util-waiter@npm:4.2.11" dependencies: - "@smithy/abort-controller": "npm:^3.1.2" - "@smithy/types": "npm:^3.4.0" + "@smithy/abort-controller": "npm:^4.2.11" + "@smithy/types": "npm:^4.13.0" tslib: "npm:^2.6.2" - checksum: 10/6e0ae10a7c7d8284d5e91eb362f726fd0406c9ddae655d2c9a40f429ef7805f21b9688db9c249975be50d1ecd6311b7513c387af86e48a2c421fa258f27d7be7 + checksum: 10/b33b552631a06c3b6787a27d4d9df2a32119f084696faad4cf470dcc8d45133926dd87a0d918f08043ac3ee35c946dd6b6cf9836b45751159a91f450b46fa02e languageName: node linkType: hard -"@smithy/uuid@npm:^1.1.0": - version: 1.1.0 - resolution: "@smithy/uuid@npm:1.1.0" +"@smithy/uuid@npm:^1.1.2": + version: 1.1.2 + resolution: "@smithy/uuid@npm:1.1.2" dependencies: tslib: "npm:^2.6.2" - checksum: 10/fe77b1cebbbf2d541ee2f07eec6d4573af16e08dd3228758f59dcbe85a504112cefe81b971818cf39e2e3fa0ed1fcc61d392cddc50fca13d9dc9bd835e366db0 + checksum: 10/35b77a2483a37755c2be1faf66036f5e0b7939a7c608b93982fce9d4f137f1778784f101a2874a6756d9fd25092c6a95dd07314df12dcb9a0a03244b4cc4d8c4 languageName: node linkType: hard -"@snyk/dep-graph@npm:^2.3.0": - version: 2.9.0 - resolution: "@snyk/dep-graph@npm:2.9.0" +"@snyk/dep-graph@npm:^2.12.0": + version: 2.14.0 + resolution: "@snyk/dep-graph@npm:2.14.0" dependencies: event-loop-spinner: "npm:^2.1.0" lodash.clone: "npm:^4.5.0" @@ -19387,10 +19226,10 @@ __metadata: lodash.union: "npm:^4.6.0" lodash.values: "npm:^4.3.0" object-hash: "npm:^3.0.0" - packageurl-js: "npm:1.2.0" + packageurl-js: "npm:2.0.1" semver: "npm:^7.0.0" tslib: "npm:^2" - checksum: 10/60228f3b0999b42139907f7be67aa6769a4885ca6a291ebaa4d4f296e653f60e0e291f7dc2696c658fea76f086583cc61d67f34fc1d1e5dacf00f6ca46362d21 + checksum: 10/e51882a2e566030a135d2fff477afbc7e4f8a14946089598fe56dd1e9b1b65fffe1ef8b3c8e2ff42629fc01d79b6d5dfeca1c505413731866c5b730728aee0a4 languageName: node linkType: hard @@ -19762,79 +19601,83 @@ __metadata: linkType: hard "@storybook/addon-a11y@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/addon-a11y@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/addon-a11y@npm:10.3.0-alpha.14" dependencies: "@storybook/global": "npm:^5.0.0" axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^10.3.0-alpha.1 - checksum: 10/866033fb0caa993159f280e5bf5af29d642cab47e0c30db23f074a9ebaf937edf41a54da779e452399e7923d13e8a93b7f361b471f6c3e79d1e5ba07f4ee62f7 + storybook: ^10.3.0-alpha.14 + checksum: 10/4c5b25aad081653d1ce76486e6dc0c07d62e390856607c348428a9875b97f90e30d802d19763b6ac00c9fb529279176a2864b3fd855410da5ef5eaea613a1954 languageName: node linkType: hard "@storybook/addon-docs@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/addon-docs@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/addon-docs@npm:10.3.0-alpha.14" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/csf-plugin": "npm:10.3.0-alpha.1" + "@storybook/csf-plugin": "npm:10.3.0-alpha.14" "@storybook/icons": "npm:^2.0.1" - "@storybook/react-dom-shim": "npm:10.3.0-alpha.1" + "@storybook/react-dom-shim": "npm:10.3.0-alpha.14" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.0-alpha.1 - checksum: 10/2eccaeea3c725bba93442e6bdebe6c432bbc6219f41013ee679d87f3814d51bf1f7829a34587563797952dd9d741db3e39e91e92c303955188c9c42302f7c006 + storybook: ^10.3.0-alpha.14 + checksum: 10/a99769d506c0436c44d6d6c156c79063a8d4d521c09a74836880a696a9d51ef646fa6789de3edab391cd9379c96b8df488d01c032f088cf2a6edaef8273a7886 languageName: node linkType: hard "@storybook/addon-links@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/addon-links@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/addon-links@npm:10.3.0-alpha.14" dependencies: "@storybook/global": "npm:^5.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 peerDependenciesMeta: react: optional: true - checksum: 10/d50c997bc5edf6289fb7967c0dc19c370422b3b02289642efda0f7fe2977aef16c4f1b11e7a0245ab8a315df4d98196c6d194f178266a0ada76d47604aba98bc + checksum: 10/a7d8933c43b44406b12d521e6054a35dd8fe4f40b872e8f3c9e619dba6e06243f8b51fc8efa9777c554ed1059f5d4cc9f4d186fe1b33398490cab1d39e2f4810 languageName: node linkType: hard -"@storybook/addon-mcp@npm:^0.2.2": - version: 0.2.2 - resolution: "@storybook/addon-mcp@npm:0.2.2" +"@storybook/addon-mcp@npm:^0.3.0": + version: 0.3.4 + resolution: "@storybook/addon-mcp@npm:0.3.4" dependencies: - "@storybook/mcp": "npm:0.2.1" + "@storybook/mcp": "npm:0.5.1" "@tmcp/adapter-valibot": "npm:^0.1.4" "@tmcp/transport-http": "npm:^0.8.0" picoquery: "npm:^2.5.0" tmcp: "npm:^1.16.0" valibot: "npm:1.2.0" peerDependencies: + "@storybook/addon-vitest": ^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 storybook: ^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 - checksum: 10/dc6d8e97307f0f7720e5fe7876f9cc5a4d66cebe9259f55b713ce8386fb5a25874d6c0b011afb5a1ab45e3b9a3eb1f590996f49f85c3feaf1a791b80d1fc65c9 + peerDependenciesMeta: + "@storybook/addon-vitest": + optional: true + checksum: 10/0fb377d8ee96055703cfee48c4a47d25367a090b4e343a0d5db15341137463ae01f167725e7087bc20fe072b474a798a6488452a3afce1b27c47049e65e56177 languageName: node linkType: hard "@storybook/addon-themes@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/addon-themes@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/addon-themes@npm:10.3.0-alpha.14" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.0-alpha.1 - checksum: 10/801550d5a64075142d278985be22922fa93beee8bcba8b6c7c8b6927b78fe64f8c99f03109868712bb53df5419b8aeb89ec25d3bf1820ea6ef6cc4b1de32fde2 + storybook: ^10.3.0-alpha.14 + checksum: 10/fad0807d2b7750216459a0f771af08c1a6a02252046e88a7ff4f307dc99d96930c63a9f6fe922f984261f9ac42c745fa1c5baf2f5bfa3b739ed1120af8ec950a languageName: node linkType: hard "@storybook/addon-vitest@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/addon-vitest@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/addon-vitest@npm:10.3.0-alpha.14" dependencies: "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^2.0.1" @@ -19842,7 +19685,7 @@ __metadata: "@vitest/browser": ^3.0.0 || ^4.0.0 "@vitest/browser-playwright": ^4.0.0 "@vitest/runner": ^3.0.0 || ^4.0.0 - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: "@vitest/browser": @@ -19853,32 +19696,32 @@ __metadata: optional: true vitest: optional: true - checksum: 10/2d8ed11531f7e3b520c56e61506a338291d2bace0f15233b151508569e7fa3922dd9ab1be05401a8866c8a309a8e0ba5df99b42876cf13137404d445a04cf88d + checksum: 10/ff0b388ee6fe2b1d1371b7eedfa5d9c0783e72525d405cc777df1493e191a59048327d830fa281a005f882463bba4aff905f8319485e1b1ca175af3030353f04 languageName: node linkType: hard -"@storybook/builder-vite@npm:10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/builder-vite@npm:10.3.0-alpha.1" +"@storybook/builder-vite@npm:10.3.0-alpha.14": + version: 10.3.0-alpha.14 + resolution: "@storybook/builder-vite@npm:10.3.0-alpha.14" dependencies: - "@storybook/csf-plugin": "npm:10.3.0-alpha.1" + "@storybook/csf-plugin": "npm:10.3.0-alpha.14" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10/fc3697c068fdd4375589b4de98e8c3f09e5fe4bb9c1b2bff12b19abbf04f08d62c1b2794aaf9f599acd4feecfe02bc9b92a315c5e52dc6372491e40818f4449a + checksum: 10/2aad8a27c44770b975c8acfba0b254f17f624afa984a6c0ca35a351e94085fe5e91a0123d912e6275f8a796bc78e760a6f0c1457b3a140544c0409b353be823e languageName: node linkType: hard -"@storybook/csf-plugin@npm:10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/csf-plugin@npm:10.3.0-alpha.1" +"@storybook/csf-plugin@npm:10.3.0-alpha.14": + version: 10.3.0-alpha.14 + resolution: "@storybook/csf-plugin@npm:10.3.0-alpha.14" dependencies: unplugin: "npm:^2.3.5" peerDependencies: esbuild: "*" rollup: "*" - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 vite: "*" webpack: "*" peerDependenciesMeta: @@ -19890,7 +19733,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10/9ab9ddd3b8aeaf1301828b3e6e3c16a6c8b216e6e73ed0063520f59a2dd2f30cb1d492520d170aa3c064d5f8ad2a27557a858536c1039156799dcafd99123b0e + checksum: 10/e388b4b8781d0c8322206ee1b89b981bf6b5d354b5066ab2868ca9aba88921473194d2ad14834a398f96678b10b068199f22e73bc43eb49597b6d5bd58d32a84 languageName: node linkType: hard @@ -19911,37 +19754,37 @@ __metadata: languageName: node linkType: hard -"@storybook/mcp@npm:0.2.1": - version: 0.2.1 - resolution: "@storybook/mcp@npm:0.2.1" +"@storybook/mcp@npm:0.5.1": + version: 0.5.1 + resolution: "@storybook/mcp@npm:0.5.1" dependencies: "@tmcp/adapter-valibot": "npm:^0.1.4" "@tmcp/transport-http": "npm:^0.8.0" tmcp: "npm:^1.16.0" valibot: "npm:1.2.0" - checksum: 10/fc2cbd82c1e8ed4dbaebdc4db5b7142516a2a1cc7e91e578a7bf9d8b7c7f01072ba69c335fd0cec58b849404da95c3f637615c95587381b1a310b2e10b233d23 + checksum: 10/a5924be156db45ac86ee57c325878ee1a46cf3bf9523fc9f231d8f491e3a77f92d74801d138a4af6256cebcd74c8aa5985d0c9fded4fcbdbd4708ccdcc70ee31 languageName: node linkType: hard -"@storybook/react-dom-shim@npm:10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/react-dom-shim@npm:10.3.0-alpha.1" +"@storybook/react-dom-shim@npm:10.3.0-alpha.14": + version: 10.3.0-alpha.14 + resolution: "@storybook/react-dom-shim@npm:10.3.0-alpha.14" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.0-alpha.1 - checksum: 10/74d5d7d1b43b35a02bbbf0c75cdb3becb0f9f17d511be1c79fd6ab1c0c0dc3d0f83b04a43e53ecb02e6c34a7734050649241f1b18cd037b9fe59f9088b182b35 + storybook: ^10.3.0-alpha.14 + checksum: 10/3adcc9c8f6b3cb3514eeb2d75efb15de6878915d181004bbc81df14ccc6ab3361f8a025f3c780fa385c73b571bb94b592ad9d1811897013b78206fe623769f82 languageName: node linkType: hard "@storybook/react-vite@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/react-vite@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "@storybook/react-vite@npm:10.3.0-alpha.14" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:^0.6.3" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:^0.6.4" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:10.3.0-alpha.1" - "@storybook/react": "npm:10.3.0-alpha.1" + "@storybook/builder-vite": "npm:10.3.0-alpha.14" + "@storybook/react": "npm:10.3.0-alpha.14" empathic: "npm:^2.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^8.0.0" @@ -19950,356 +19793,421 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10/dfab9b3259a1fc44e3ff62732fade697bf9dc17896f01baa8084a13492507b155d586606e7e213338cdc60107b669ff3542dfd6a44cb0574a8196cb892ece9e1 + checksum: 10/bce268423eaee1df408b7753e67d02220fd5cf130d3bf918db2f661fcd210a80822313eff8a6f565aaba91d05db9a6ed53d7235851a3ef97b7e07e214367a820 languageName: node linkType: hard -"@storybook/react@npm:10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "@storybook/react@npm:10.3.0-alpha.1" +"@storybook/react@npm:10.3.0-alpha.14": + version: 10.3.0-alpha.14 + resolution: "@storybook/react@npm:10.3.0-alpha.14" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/react-dom-shim": "npm:10.3.0-alpha.1" + "@storybook/react-dom-shim": "npm:10.3.0-alpha.14" react-docgen: "npm:^8.0.2" + react-docgen-typescript: "npm:^2.2.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.0-alpha.1 + storybook: ^10.3.0-alpha.14 typescript: ">= 4.9.x" peerDependenciesMeta: typescript: optional: true - checksum: 10/be7682725f97b207cf43d06b5435de4501d9c7cd3174d3988775bdfa49a846f0eb73587780f84f8be9be6270798c423cc882c6d0e394513de777589f1bebd4c2 + checksum: 10/4278726e1c4612ea68d3a0a8d6ef2ff3b9b31c7e93dd7e01178f467c3e879e9b2ceb3462d3232b1bcf4b96968eb5e9f8abca6881959a5c7c1f9be8cc6d1cf4d9 languageName: node linkType: hard -"@swagger-api/apidom-ast@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ast@npm:1.0.0-rc.4" +"@swagger-api/apidom-ast@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ast@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-error": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" unraw: "npm:^3.0.0" - checksum: 10/77be60bd4085525de506eb31a4f67de7850296534b98cb981db5a9e3ac8ebe07b92cb6ff8edacc5f199373f8dab9bc845b6695ae4a40dc15d0e05a05c8b3041f + checksum: 10/72a778c5fe6b679561d336718ae2e2c4ed10d3dd4169d8ca804a46fa3d5165c9a6344c7716ec5444d3e51ce69709a98e1fcdd8fabb05836575eed6c5d953d007 languageName: node linkType: hard -"@swagger-api/apidom-core@npm:^1.0.0-rc.1, @swagger-api/apidom-core@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-core@npm:1.0.0-rc.4" +"@swagger-api/apidom-core@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-core@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-ast": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" minim: "npm:~0.23.8" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" short-unique-id: "npm:^5.3.2" ts-mixer: "npm:^6.0.3" - checksum: 10/73763b2888aa1e7642dd08192fa91a6c5049575856bb6916333bf194f1798706b424e38efd39d9729ce95ec1fe545407c6ec3a9212e360d57af02f003f077643 + checksum: 10/f92fba789715ac128f4b9f6ebb99a2739a2610efc74e4e5ba018295974c7b94430930bfc949a776a9279c2c9154c51d738461d16a644dd0427e7344d36d3750b languageName: node linkType: hard -"@swagger-api/apidom-error@npm:^1.0.0-rc.1, @swagger-api/apidom-error@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-error@npm:1.0.0-rc.4" +"@swagger-api/apidom-error@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-error@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - checksum: 10/dfbc2e9d570eb15e925a4eb0dd4e59f8d5a01cfda4a78dd83320c71f3406082b73a6ea2b61229730a3830a707fdb94b17b3b0cb0ffaf45699917434b36971409 + checksum: 10/00b1c7509ce25f817440b69bd3b03caddd2861490f995dbf7c2d5fb03d503e58fbba65c7d3817c5c0b237f19ca7e3617497d6dea1211e2b220a34594a3c71ed0 languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:^1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-rc.1, @swagger-api/apidom-json-pointer@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-rc.4" +"@swagger-api/apidom-json-pointer@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-json-pointer@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" "@swaggerexpert/json-pointer": "npm:^2.10.1" - checksum: 10/cd8ea73069934422a7c48a3ca6a61201e3acfc35cd8cb8df016c4bcfb655b6ba21abee5011edbe4cc1fa83076e1d1c3031d8e9384b3e0b2e6e0b103fa3ea96cc + checksum: 10/28d5d650d21f2eb998a8f18612c9edd0cf71d690abc387a72efd5bd49690dc48c71dd390954fc8f9ba6fced8e357fd12f69e705b44c4091b9f924e740c02314a languageName: node linkType: hard -"@swagger-api/apidom-ns-api-design-systems@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-api-design-systems@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/e30c5da34485eeb62be9b37ccc58bc11d2f4559799369a391157c42b2ff5bbb46e9a931b9e0dcd856912ad4078bc2631f6b272464e77709fdfd8c1725c780d3f + checksum: 10/0763ec9275e661549b845d149fbc9b8307374a343c376b0068688c6506231678461964477dc4b63de0b3b7da25a3160f57254db90d1ba75eb0354e74f2ca8e0b languageName: node linkType: hard -"@swagger-api/apidom-ns-arazzo-1@npm:^1.0.0-rc.0, @swagger-api/apidom-ns-arazzo-1@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-arazzo-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-arazzo-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-arazzo-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/4c56afb584d79e7be530b5aba31cf4556d5fbefeafcd8d25c03fa74d895bafb99e8c9d266c3b09f06758c7bbcff6a1367105240e238cd56312e83e1f8393430c + checksum: 10/1cb5d4bc96caf506402f043fc9b3e551a8c777bdc7e241c22112ad01d1f61f89e78bbaebbc6b2376d507031c4a8de00c92e9d1f019c677d157b66e31ed9ad501 languageName: node linkType: hard -"@swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-rc.0, @swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-asyncapi-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/94ea647db429877c914dc6449165570ea13233d81f9b3b28f6d5d035b7cccdd53f7bf50279ad97b42a91f20d109d50a040e32fa571b1a1f5793e339e4426c6db + checksum: 10/4b701afd154f78ec00bac4a2be4f804eb56cb72a3cf5e33fb6bcfda495d0b2745676a63aea5d44b03d3c92398b259831cafbbb99465467472b5ad270e4431bc7 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-2019-09@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-json-schema-2019-09@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-asyncapi-3@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-asyncapi-3@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.3" + checksum: 10/a63f58441e09a9d68017a5d927c486c3602fb66355bffb17a3179f396736af3acea43946dba3633ac03c325e74bfee00c6a57574661ad3c5c259a88c990cd902 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-2019-09@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-json-schema-2019-09@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/8760f7252a31eaed22417f3f181e1f29f25c9100044a78b4f1c3690ddc6402845ba5c90ee98297dc56f7b3d3758c4fb086c53c204b7d1c8f87faebdf57654ea5 + checksum: 10/09c810db219bb67798f6240fffa5ecc24f2a328f0b77cd8d1afbf4a76df19c7ef23b073b78939d747fb81e27c66251ba2479275111c1b6aa78051464a1b264c2 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-2019-09": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-2019-09": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/05d31abb732435cfbdc6cc46332f4a2d90462e11c103fefa22f669827c99916363bc569adce0fcbbe52c51061c490006f8ca20bd1420ecfea680ad35589f8390 + checksum: 10/2a7d9c24f95e0731a96d66d8fcf434538816d7f920b0b0cfda00c99d9ad1381be943675232c13a55aa3aeae0e22de3e10e8a367c2eeb3f0271e8362469618011 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-ast": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-core": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/37223af3cf5e6feff7021e40129eacda2f89f7a176466bfdef12f2785b8963a4583ff8466215af7b2f7508515c29baa86a549ee1449a7b40afee7c84c27f5ec8 + checksum: 10/82672d4125d77f18f4e81d84224ba1a3cd286e00ab824eb7339a7721c034638b6309328e22796c702f06ff22509b80bcb48a39bbe60a262b3f548c0c5bff182f languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/9294144d88e1bac74f26c99c3b7b1a0dacdf5e7823360d6ae18918c67a20ae42b297c00b4d641a803bd90ad6c1e42390b4d1ef1de5916becead4fee1e4c0507c + checksum: 10/b17d3117096ddd5b0f9bede7c335562d79b5815a6409405f87310cc23a302de50a1e6ea39dead436a1a6b5635e6a61be002ebbaf2019d63b8408c45cc50ef9d1 languageName: node linkType: hard -"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.4" - checksum: 10/53111c3c6f3ba7ae21863b70afdd0eeea4db9b571602c9ec79e4525aaaa83dde32907c8a05ce658e5244b4400ff554dc7aca9c73a9a1b91cfffecf0f09bf4f48 + checksum: 10/f2b0d3cd115d898627888c03d0301e34607f1e952012264b9a78dc84416c36efe454d87d83ff23fa52633c8fd0a051cfebd010d35d55f1d9b36d2895283c73e2 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-2@npm:^1.0.0-rc.0, @swagger-api/apidom-ns-openapi-2@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-openapi-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/42dbedf81c96c2e3e851b234777772e8662cf4957f3d1cc81dac0356a33840652e61011133fd404b0465f3a238451878cdb53182f9d295d53ff6a3e97ed94265 + checksum: 10/324dbf81b7e2850c45168188bc27fa56ed2991903a10c46843a41b08a23bac2c9ed43fe4a422e04c9a7252b4859a819f40efd2c2c7a202639c7c074d7ae80503 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/91211128aa583a1d155789e4e7e5a5fc7a7adc1fc69f836e5b742168d7f60e2c3a961ab449b5cbf47c803e536cf3490ff4ce44608d9834c2d06eeb9eb0b3feef + checksum: 10/1d0a5453b3cd13a9e8acdf97d270e9c5abf20a217d6ee051afe7f39066dbf6f8897c09305499b36173f7d24b039806b31693029f640bc5c9e2d256c725f93be8 languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-rc.1, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-openapi-3-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-ast": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-json-pointer": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/11739023a16d5aa41766aa24d094799e41b80644b4129ea3df9d606343abc6215f0350062ccb709d5361b7f2e1f516d0f2fea7c53cd1a938b1d4c2f859ac1a4b + checksum: 10/b97037cd1d40f4f74388562daf874a5a01ba4914b163a70ebe942eb31172c6a0eb2578ba97bd1810a4a78563a6c1c1750e38660c5f0b89877f774d13a2cc83d3 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:1.0.0-rc.4" +"@swagger-api/apidom-ns-openapi-3-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-ns-openapi-3-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-json-pointer": "npm:^1.6.0" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/edb734b20658354c865cc6b12315450be0d7ac340fce80185cab8f476ae9ec8983d13042788132f24ff9b32c4edf292d28c1e88a1652b927ec6e6c4679f7cce1 + ts-mixer: "npm:^6.0.3" + checksum: 10/fafca923e6b535c756df816475383c8034d2dde6ac4d966363f2a980116e79c5f8b1ea4cb25a4e359ab2661e3b3864bec1868ffe70305cd9b6cece8c34e16254 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-api-design-systems": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/8477220d192b17eb165096cb8fe699d48b554f2eb0ef737218f7136100a2a3cb7dfbcbf954dab4244796d48d0e82154b0788cfd9fe36226dcc85b2a6e11f65f0 + checksum: 10/8503382273380338fd43bcdd66827078381e0abdc3989bd54619f59342ba6c895fc2745c7b523415ec121de4748719e47154e3dfe1075f921d93435331b44454 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-arazzo-json-1@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-arazzo-json-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-arazzo-1": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-api-design-systems": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/545d8883a0c5eedfd99c4a85347355865c701eed114cd75df54965174f07f2f4a884efd61e836d2168bc859dddfbbd168857dd0f3721b0d397f4c424bd0312ec + checksum: 10/76f0711511fda6348915c905d8ba29cf9698d68a9a655eaa3f7cdb52458d16904782cb379b438c40cb047bc65123e5824f4e055df2c72d7fee7fad259e80e1a6 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-arazzo-yaml-1@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-arazzo-yaml-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-arazzo-json-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-arazzo-json-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-arazzo-1": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-arazzo-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/acec60d1874d22990b021b64458f153c080a15239c84f1ed4a710ad7480d2a7c5a8c04cb895ae874e83bf467865abc6e93d30da2f58aee0921fcab375b4565f6 + checksum: 10/4b44885c9eec28c0d93d46db2387285d14f14aa100bf6154cc7b6a2a2069bc15c1ed6f0fda7083a773a1e5c5d06bdd4e30809444c7d5e5f1dc44664950aae912 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-arazzo-yaml-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-arazzo-yaml-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-arazzo-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/16303fe4e38f993e25ad96d11f28a5b1ff42dd75ca2401b85b17f7937bbdcf7fb645c96644d3a70bf1c79d615609df4526f073c26a7719b5a4c4de50bd21f51e + checksum: 10/98cd104542af2531e321bf68c6d93be774ccaa2b2d0974ff48f664f04eb3608ea5081e8e31b685b4547c8278a348e0b9d04edfbd5affdcdcf59853fbfc4347dc languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/0894af80561bf688bc8fa06bf21e19e6b1ad6dbbe7c2c1d8f3e449f63d8d2d6e437647def9ef92ca6044df4d46a75b9581297cae655e1489b50418ac3e8de90b + checksum: 10/7d10c58c8234aeab86c77ad9bc99c511d5ced0b15db5da28c268cbfb0569fede419636046cf095cd57684a344d931835dec9bf3a720273fa983d0a4a42c5038d languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-json@npm:^1.0.0-rc.0, @swagger-api/apidom-parser-adapter-json@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-json@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-asyncapi-json-3@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-3@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-ast": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-3": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/571156ce7c28e34dce3e5144c081f81ca9b42c9fdbb712dbcaf69392f1e384bddad2b404ab8c0e6348340732418f06fb0e3bccdae12146725bf3a31f60020bbb + languageName: node + linkType: hard + +"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/0f4cb552becfc589f0ddd0e42f44e85f1ae83b8da8a896ea5c34c28ad0584fc18823d66f3a67c4110cf928fb54b7c4aa63fe3d8c73081aac1433bb19805812e6 + languageName: node + linkType: hard + +"@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-3": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/5595d5feb6e623cc70ec650d55384736fe328d41d5ea977db1d3eb848d5c7afd2a7a53d2d441efd4a97461dc5045b0ca6dd9799fa0f9ef37ff7516d89ac0d30c + languageName: node + linkType: hard + +"@swagger-api/apidom-parser-adapter-json@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-json@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" node-gyp: "npm:latest" ramda: "npm:~0.30.0" @@ -20307,149 +20215,183 @@ __metadata: tree-sitter: "npm:=0.21.1" tree-sitter-json: "npm:=0.24.8" web-tree-sitter: "npm:=0.24.5" - checksum: 10/7447cdc9e19a585e3e7275ae6a560c6ab137c39b5d5548bec4a1499b178d669cd78ed27d4b5f24187e1f09d840b5735ae01b24e6cfe7b0ed9920a587a9b16109 + checksum: 10/0ca2c1d5488fd76aed0482b1716aabebd1cdde61e7d385a7ea7e204562385043f598429edb8f87279672269a9b058ac16c94755db55ec3916403a22db155b077 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-json-2@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-json-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/85f11179c6ebd7dbd7a99cf969e6c5b67ea0b95d64d8a41b83d87ea8db2ec95e23fd0481bbb365bbfedb5f8d7cdf6c193a8120562d9f104e1395272c52feca74 + checksum: 10/8cdf4ce932611eb07f23fbaaadfb6267171f54f82f2a5966b7623bba7c4b3e1453a346e0289438782db1692bfb6d8ab2247474eaea536627886c7ad5111557da languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/fa701aa34dfb6f8088a8b896ed4ee2dfdfbe0b0bab80914e8aa85594de1babce65cd37256d70bc6b46eaae726d9dcbe0f564fd6a8711c44997729b724c85adbf + checksum: 10/8008051ea25c555a2a567ab6e209bab359eeaea03c5c8775dbda4bc9e3e23f47f0a44a43db8feaf17293ea88a86d82eef83be7ef582702e03a73f8d60cf4c010 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/382a36f76315862ceeb15b4e16e01f3e8aaf9b846025653c6a8a81230e8ac05a93ae1f9f1ce1fea76ca7ee2a8c5f0577f33ae8913cfb02bac23fc74850c47d05 + checksum: 10/f1cf4654f1961cd4a26afda093d5f907f491887e950e9dd7a30109648da38c41672b44009a39e255e0354acda6c729daa9367d6bd1a352859f8c4f1760e0f9ad languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-json-3-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/8236aea1dabe5d968a0111a196e7799ad20e3957583bf6f01bb63008ff8a0aa33db457133327be0db4d872a6a844e9cfc4c1962d3b23090a3588a0498ccec3ee + checksum: 10/5fb16b4e9d353d62c360d53658a365ec3ed61fbb09b4664699cd5152661a5d1917fe33efbd555cf9553d7b0397f380c9cefa1d30047578730b4912ffd54e1d10 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/5286670c5c7b1fad5e97f0da88e40870ba98e2511334eb32c99d9b8d8059e6cc77b2ca6f3e539a9db2cd75809d2695c4000288f14a00d00a38107360ee85825c + checksum: 10/e0512203b5c8071204da3fae6e882dfc07807f0a6de5faf57584ec089b3c543a0043dfb772ec2e2a09aa21fdadcb13bf8055bed14ca28be8a63c7c4bdd02e301 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:^1.0.0-rc.0": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/bdce51f49c2784217237e667ddc4c111095ed7dd11cef94b3383ddf6a4a0e886492546a301aa803fdcfc7fb9923f5cdcfaba305cc98fcfeedd02d85e749a80f8 + checksum: 10/daf3d67372a1f8805e5b612c4fd82ecda89fe0c74a75968830a05fc61d4d3e87174de4d26e3ccf3c1ad334c893470e854c69ea3b6f9cc0a6db95a2e2d3ef35c4 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-rc.0, @swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:1.0.0-rc.4" +"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-ast": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/58995dfe113abf9ba0301e3a9fdf72a494e87526f87ccdd170e659545489c68bcd362d0f9fa31fb99cdb7f8e400fab75b02c3222798a3d3cacee30064a7fb181 + languageName: node + linkType: hard + +"@swagger-api/apidom-parser-adapter-openapi-yaml-3-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-2@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/793c8222647f9582b74128feab6b886904768e3f02f65266af220785b7adfa18978f861735b3514d1817a7df7bb56f467079acf4bd75efef181ca678b9dbb61c + languageName: node + linkType: hard + +"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:1.6.0" + dependencies: + "@babel/runtime-corejs3": "npm:^7.26.10" + "@swagger-api/apidom-ast": "npm:^1.6.0" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" "@tree-sitter-grammars/tree-sitter-yaml": "npm:=0.7.1" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" tree-sitter: "npm:=0.22.4" web-tree-sitter: "npm:=0.24.5" - checksum: 10/c47bf36c8c2cbb8e2a0004167199d1a79df2c9c3cb97e50d7e55c2aa869546ad4376e067593b045a71d92df708b96bdadc0e7878dc60e547b9345e54384fb096 + checksum: 10/de1823718a080ab938d665011760eba042679b953355b58b6e81de5a45f00e0e8e1d8eb4133964bf788986a59e34bc086c395ce59ffe8da57836af1930697300 languageName: node linkType: hard -"@swagger-api/apidom-reference@npm:^1.0.0-rc.1": - version: 1.0.0-rc.4 - resolution: "@swagger-api/apidom-reference@npm:1.0.0-rc.4" +"@swagger-api/apidom-reference@npm:^1.6.0": + version: 1.6.0 + resolution: "@swagger-api/apidom-reference@npm:1.6.0" dependencies: "@babel/runtime-corejs3": "npm:^7.26.10" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.4" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-ns-arazzo-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-api-design-systems-json": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-arazzo-json-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-json-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "npm:^1.0.0-rc.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-rc.0" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-json-pointer": "npm:^1.6.0" + "@swagger-api/apidom-ns-arazzo-1": "npm:^1.6.0" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-2": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-asyncapi-json-3": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-3": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-json-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-json-3-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-2": "npm:^1.6.0" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.6.0" "@types/ramda": "npm:~0.30.0" axios: "npm:^1.12.2" - minimatch: "npm:^7.4.3" - process: "npm:^0.11.10" + minimatch: "npm:^10.2.1" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" dependenciesMeta: @@ -20465,6 +20407,8 @@ __metadata: optional: true "@swagger-api/apidom-ns-openapi-3-1": optional: true + "@swagger-api/apidom-ns-openapi-3-2": + optional: true "@swagger-api/apidom-parser-adapter-api-design-systems-json": optional: true "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": @@ -20475,8 +20419,12 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-asyncapi-json-2": optional: true + "@swagger-api/apidom-parser-adapter-asyncapi-json-3": + optional: true "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": optional: true + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-3": + optional: true "@swagger-api/apidom-parser-adapter-json": optional: true "@swagger-api/apidom-parser-adapter-openapi-json-2": @@ -20485,15 +20433,19 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-openapi-json-3-1": optional: true + "@swagger-api/apidom-parser-adapter-openapi-json-3-2": + optional: true "@swagger-api/apidom-parser-adapter-openapi-yaml-2": optional: true "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": optional: true "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": optional: true + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-2": + optional: true "@swagger-api/apidom-parser-adapter-yaml-1-2": optional: true - checksum: 10/0efab87df6254abb4259912e6f845420b9fbd33c6a0491fdba5c166f0fcee880bdcea898e03b92b20bb5bf6251047df986797f6471000633a7dd448968e1de50 + checksum: 10/c522d87d226554a7844705a1841c27cc5daebd4f1cc6f01c0d39d65bad96865408b8ffdbdc98f99f976dc9d1278f7b6d887ca00b09bdb0f8cf02fbcd257cee43 languageName: node linkType: hard @@ -20515,90 +20467,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-darwin-arm64@npm:1.15.6" +"@swc/core-darwin-arm64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-darwin-arm64@npm:1.15.18" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-darwin-x64@npm:1.15.6" +"@swc/core-darwin-x64@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-darwin-x64@npm:1.15.18" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.6" +"@swc/core-linux-arm-gnueabihf@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.18" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-linux-arm64-gnu@npm:1.15.6" +"@swc/core-linux-arm64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-linux-arm64-musl@npm:1.15.6" +"@swc/core-linux-arm64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.18" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-linux-x64-gnu@npm:1.15.6" +"@swc/core-linux-x64-gnu@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-linux-x64-musl@npm:1.15.6" +"@swc/core-linux-x64-musl@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-linux-x64-musl@npm:1.15.18" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-win32-arm64-msvc@npm:1.15.6" +"@swc/core-win32-arm64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.18" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-win32-ia32-msvc@npm:1.15.6" +"@swc/core-win32-ia32-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.18" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.15.6": - version: 1.15.6 - resolution: "@swc/core-win32-x64-msvc@npm:1.15.6" +"@swc/core-win32-x64-msvc@npm:1.15.18": + version: 1.15.18 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.18" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.15.6": - version: 1.15.6 - resolution: "@swc/core@npm:1.15.6" + version: 1.15.18 + resolution: "@swc/core@npm:1.15.18" dependencies: - "@swc/core-darwin-arm64": "npm:1.15.6" - "@swc/core-darwin-x64": "npm:1.15.6" - "@swc/core-linux-arm-gnueabihf": "npm:1.15.6" - "@swc/core-linux-arm64-gnu": "npm:1.15.6" - "@swc/core-linux-arm64-musl": "npm:1.15.6" - "@swc/core-linux-x64-gnu": "npm:1.15.6" - "@swc/core-linux-x64-musl": "npm:1.15.6" - "@swc/core-win32-arm64-msvc": "npm:1.15.6" - "@swc/core-win32-ia32-msvc": "npm:1.15.6" - "@swc/core-win32-x64-msvc": "npm:1.15.6" + "@swc/core-darwin-arm64": "npm:1.15.18" + "@swc/core-darwin-x64": "npm:1.15.18" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.18" + "@swc/core-linux-arm64-gnu": "npm:1.15.18" + "@swc/core-linux-arm64-musl": "npm:1.15.18" + "@swc/core-linux-x64-gnu": "npm:1.15.18" + "@swc/core-linux-x64-musl": "npm:1.15.18" + "@swc/core-win32-arm64-msvc": "npm:1.15.18" + "@swc/core-win32-ia32-msvc": "npm:1.15.18" + "@swc/core-win32-x64-msvc": "npm:1.15.18" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.25" peerDependencies: @@ -20627,7 +20579,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/d17d1b5e6e81f8c262b668f237b863dd612eaa19fc35e74067b2a27ba3998c0c511ec7f49cd91c02f61fa77dd1b5d7e65d293736f65ec45d6c13989904b1c7eb + checksum: 10/ef198b9feb6eee034e3a912c37988ece2885fec35419e8245d467adbc1fc47a5c3e61869d1bdbe6fff76cbd9186ef278120cbb9746d5f7446576f4a7f15c2dcd languageName: node linkType: hard @@ -20638,12 +20590,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.17, @swc/helpers@npm:^0.5.8": - version: 0.5.17 - resolution: "@swc/helpers@npm:0.5.17" +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": + version: 0.5.19 + resolution: "@swc/helpers@npm:0.5.19" dependencies: tslib: "npm:^2.8.0" - checksum: 10/1fc8312a78f1f99c8ec838585445e99763eeebff2356100738cdfdb8ad47d2d38df678ee6edd93a90fe319ac52da67adc14ac00eb82b606c5fb8ebc5d06ec2a2 + checksum: 10/3fd365fb3265f97e1241bcbcea9bfa5e15e03c630424e1b54597e00d30be2c271cb0c74f45e1739c6bc5ae892647302fab412de5138941aa96e66aebf4586700 languageName: node linkType: hard @@ -20716,13 +20668,12 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/config": "workspace:^" "@backstage/plugin-techdocs-node": "workspace:^" - "@types/commander": "npm:^2.12.2" "@types/fs-extra": "npm:^11.0.0" "@types/http-proxy": "npm:^1.17.4" "@types/node": "npm:^22.13.14" "@types/serve-handler": "npm:^6.1.0" "@types/webpack-env": "npm:^1.15.3" - commander: "npm:^12.0.0" + commander: "npm:^14.0.3" find-process: "npm:^2.0.0" fs-extra: "npm:^11.0.0" global-agent: "npm:^3.0.0" @@ -20753,7 +20704,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.6.3": +"@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.9.1": version: 6.9.1 resolution: "@testing-library/jest-dom@npm:6.9.1" dependencies: @@ -20818,6 +20769,20 @@ __metadata: languageName: node linkType: hard +"@theguild/federation-composition@npm:^0.21.3": + version: 0.21.3 + resolution: "@theguild/federation-composition@npm:0.21.3" + dependencies: + constant-case: "npm:^3.0.4" + debug: "npm:4.4.3" + json5: "npm:^2.2.3" + lodash.sortby: "npm:^4.7.0" + peerDependencies: + graphql: ^16.0.0 + checksum: 10/9c2f8a9e758e19e20443b146b88b1c1d8eaef4214988fe0cb33c57498090d1ab94f5bbff182d7a5f73c9613e997690093afd256c929718f8d3caea524f385a43 + languageName: node + linkType: hard + "@tmcp/adapter-valibot@npm:^0.1.4": version: 0.1.5 resolution: "@tmcp/adapter-valibot@npm:0.1.5" @@ -20923,13 +20888,6 @@ __metadata: languageName: node linkType: hard -"@trysound/sax@npm:0.2.0": - version: 0.2.0 - resolution: "@trysound/sax@npm:0.2.0" - checksum: 10/7379713eca480ac0d9b6c7b063e06b00a7eac57092354556c81027066eb65b61ea141a69d0cc2e15d32e05b2834d4c9c2184793a5e36bbf5daf05ee5676af18c - languageName: node - linkType: hard - "@ts-graphviz/adapter@npm:^2.0.6": version: 2.0.6 resolution: "@ts-graphviz/adapter@npm:2.0.6" @@ -21234,15 +21192,6 @@ __metadata: languageName: node linkType: hard -"@types/commander@npm:^2.12.2": - version: 2.12.5 - resolution: "@types/commander@npm:2.12.5" - dependencies: - commander: "npm:*" - checksum: 10/5b70bf09366f778f54cd0b831417aa1f749cdd9e95e81016d0537b801ad14275973b04c34ff4dfaa344986aa93b4518c2df12647ecf8b499b636585335ae3edc - languageName: node - linkType: hard - "@types/compression@npm:^1.7.5": version: 1.8.1 resolution: "@types/compression@npm:1.8.1" @@ -21418,14 +21367,14 @@ __metadata: languageName: node linkType: hard -"@types/dockerode@npm:^3.3.47": - version: 3.3.47 - resolution: "@types/dockerode@npm:3.3.47" +"@types/dockerode@npm:^4.0.1": + version: 4.0.1 + resolution: "@types/dockerode@npm:4.0.1" dependencies: "@types/docker-modem": "npm:*" "@types/node": "npm:*" "@types/ssh2": "npm:*" - checksum: 10/b840ae7872398a3b02e5789006a69d0cf5bb7ec6c0eb714c7ca04ca093add8de4cd06204ecd8f01388e347e62927cf4c599e8b7dba53e81c1350910da766d517 + checksum: 10/d16b3a69a20fac269b2317a978442a6752dea158729a264511a3812dcb4c756e0ee079b39b61068cee182f268661e27e32aa7a28815262f0e088ffeb9f2f48c5 languageName: node linkType: hard @@ -21445,13 +21394,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" @@ -21746,12 +21688,10 @@ __metadata: languageName: node linkType: hard -"@types/jquery@npm:^3.3.34": - version: 3.5.33 - resolution: "@types/jquery@npm:3.5.33" - dependencies: - "@types/sizzle": "npm:*" - checksum: 10/9a9e2cddc584f9afa1970b0febac0b65bed6d8084baf9655346f5787ee1b25975a0f259b0c81edc7757fbb92d584ed2d1b39804ab78ab0238407c7e4b0376011 +"@types/jquery@npm:^4.0.0": + version: 4.0.0 + resolution: "@types/jquery@npm:4.0.0" + checksum: 10/3a0491a1a2b7d0a9d471c318dcf245607c8af0954658a5d2758b9f746cd85a1ce362c37c2a3ab2a41ab24d67574e167d2f4e6a873fdceeef359536d73e03a804 languageName: node linkType: hard @@ -21886,9 +21826,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.151": - version: 4.17.23 - resolution: "@types/lodash@npm:4.17.23" - checksum: 10/05935534a44aadef67c2158b2fb4a042a226970088106a40ddc67e4f063783149fe5cf02279d7dd4a1e72c98d9189b9430face659645dbf77270f8c4c3e387f5 + version: 4.17.24 + resolution: "@types/lodash@npm:4.17.24" + checksum: 10/0f2082565f60f9787eefc046edc38458054512be5a8b3584ef0bad5fd9e85d0ab55ec5a1fbfae1ed6ba015cf1f9e837d5fb4da1f99fc60b8f74b2a46146fb00f languageName: node linkType: hard @@ -22019,6 +21959,15 @@ __metadata: languageName: node linkType: hard +"@types/mute-stream@npm:^0.0.4": + version: 0.0.4 + resolution: "@types/mute-stream@npm:0.0.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/af8d83ad7b68ea05d9357985daf81b6c9b73af4feacb2f5c2693c7fd3e13e5135ef1bd083ce8d5bdc8e97acd28563b61bb32dec4e4508a8067fcd31b8a098632 + languageName: node + linkType: hard + "@types/mysql@npm:2.15.27": version: 2.15.27 resolution: "@types/mysql@npm:2.15.27" @@ -22073,13 +22022,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^13.7.0": - version: 13.13.52 - resolution: "@types/node@npm:13.13.52" - checksum: 10/a1fbd080dd2462f6f0d0c10cb8328ee6b22e59941fb6beb8bca907f96e00798ce85e94320ccab3bf04f87d6c5443535a62e6896ac59c34c79a286821223e56cd - languageName: node - linkType: hard - "@types/node@npm:^15.6.1": version: 15.14.9 resolution: "@types/node@npm:15.14.9" @@ -22103,6 +22045,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.10.7": + version: 20.19.37 + resolution: "@types/node@npm:20.19.37" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10/7e0d561d0d980109a0b64c6f797584a69d706421f18c0b24850206a15a4286443e676032abfa438590f25e4dfc3561ffc8d7f5c0c90edf08b705f6f9c35febba + languageName: node + linkType: hard + "@types/node@npm:^22.13.14": version: 22.19.1 resolution: "@types/node@npm:22.19.1" @@ -22113,11 +22064,11 @@ __metadata: linkType: hard "@types/nodemailer@npm:^7.0.0": - version: 7.0.9 - resolution: "@types/nodemailer@npm:7.0.9" + version: 7.0.11 + resolution: "@types/nodemailer@npm:7.0.11" dependencies: "@types/node": "npm:*" - checksum: 10/6bfca388af668a05a68e750a7acbd5736a3dcdf96f34f23bf4211ab152572d55008fd813e38bf03f944c6edbd002a7f98d6cee048343b8e840b3b91a00befac9 + checksum: 10/a657038e919603f1e7136bd47c631c4ef2857576f57d5024d37a2581d4993ef352681e9264604747db7055347e19818add8ab9f964be0f116dd437f5d31ff0e5 languageName: node linkType: hard @@ -22235,12 +22186,12 @@ __metadata: languageName: node linkType: hard -"@types/pg-pool@npm:2.0.6": - version: 2.0.6 - resolution: "@types/pg-pool@npm:2.0.6" +"@types/pg-pool@npm:2.0.7": + version: 2.0.7 + resolution: "@types/pg-pool@npm:2.0.7" dependencies: "@types/pg": "npm:*" - checksum: 10/cc54ce97115effc982bd052f79901a78215e76554aca0ecc92e78eb907e4fb2962924039369cd9aaf48075f1637593ce14647c62d3a2eb03789ce5d1c6df750b + checksum: 10/b2ac51f1e98cd97ef8ee9c09f4db6bb369dfa406dc41533b13a3b7c6e3a5c8c1d52ee139f8bc453b5b5c0125d1fedea610c230696a722ec9176076455e6f267a languageName: node linkType: hard @@ -22297,6 +22248,15 @@ __metadata: languageName: node linkType: hard +"@types/proper-lockfile@npm:^4": + version: 4.1.4 + resolution: "@types/proper-lockfile@npm:4.1.4" + dependencies: + "@types/retry": "npm:*" + checksum: 10/b0d1b8e84a563b2c5f869f7ff7542b1d83dec03d1c9d980847cbb189865f44b4a854673cdde59767e41bcb8c31932e613ac43822d358a6f8eede6b79ccfceb1d + languageName: node + linkType: hard + "@types/protocol-buffers-schema@npm:^3.4.3": version: 3.4.3 resolution: "@types/protocol-buffers-schema@npm:3.4.3" @@ -22307,9 +22267,9 @@ __metadata: linkType: hard "@types/qs@npm:*, @types/qs@npm:^6.9.6": - version: 6.14.0 - resolution: "@types/qs@npm:6.14.0" - checksum: 10/1909205514d22b3cbc7c2314e2bd8056d5f05dfb21cf4377f0730ee5e338ea19957c41735d5e4806c746176563f50005bbab602d8358432e25d900bdf4970826 + version: 6.15.0 + resolution: "@types/qs@npm:6.15.0" + checksum: 10/871162881f1c83e61d0c8c243c65549be5dddf33a6911f3324edeebd4087207b1174644da9a3afaa20cf494c5288d2a1ece09e10e4822f755339f14a05c339ea languageName: node linkType: hard @@ -22485,6 +22445,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:*": + version: 0.12.5 + resolution: "@types/retry@npm:0.12.5" + checksum: 10/3fb6bf91835ca0eb2987567d6977585235a7567f8aeb38b34a8bb7bbee57ac050ed6f04b9998cda29701b8c893f5dfe315869bc54ac17e536c9235637fe351a2 + languageName: node + linkType: hard + "@types/retry@npm:0.12.0": version: 0.12.0 resolution: "@types/retry@npm:0.12.0" @@ -22499,24 +22466,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" @@ -22531,10 +22480,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 @@ -22586,6 +22535,13 @@ __metadata: languageName: node linkType: hard +"@types/shell-quote@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/shell-quote@npm:1.7.5" + checksum: 10/32b4d697c7d23dbadf40713692c47f1595f083a3b3deea76cb18e30a05d197aa9205d2b87f6d92edb60cda120b51e35d32bda96ed9b0a7e32921eed2deb4559e + languageName: node + linkType: hard + "@types/sinon@npm:^17.0.3": version: 17.0.3 resolution: "@types/sinon@npm:17.0.3" @@ -22602,13 +22558,6 @@ __metadata: languageName: node linkType: hard -"@types/sizzle@npm:*": - version: 2.3.2 - resolution: "@types/sizzle@npm:2.3.2" - checksum: 10/3247c553400c3daab142682520249d818c473a8cf77c648c1e3e6e9fc9c477f5423e2b4112dd3c564c65f96db7e2b5be8dbbc22acf4da6fa0c5a149edd2feda5 - languageName: node - linkType: hard - "@types/sockjs@npm:^0.3.36": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -22618,13 +22567,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" @@ -22695,15 +22637,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" @@ -22713,16 +22646,6 @@ __metadata: languageName: node linkType: hard -"@types/tar@npm:^6.1.1": - version: 6.1.13 - resolution: "@types/tar@npm:6.1.13" - dependencies: - "@types/node": "npm:*" - minipass: "npm:^4.0.0" - checksum: 10/d325223cf90399fd03f366d0eabe2383e75e550b3e40a006d5f062d006b894a475cd7c0968d258a8eb8eae5df30b6e7f4607d493a474f89134bbff65362b77ed - languageName: node - linkType: hard - "@types/tedious@npm:^4.0.14": version: 4.0.14 resolution: "@types/tedious@npm:4.0.14" @@ -22741,15 +22664,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" @@ -22832,17 +22746,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" @@ -22854,6 +22757,13 @@ __metadata: languageName: node linkType: hard +"@types/wrap-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/wrap-ansi@npm:3.0.0" + checksum: 10/8aa644946ca4e859668c36b8e2bcf2ac4bdee59dac760414730ea57be8a93ae9166ebd40a088f2ab714843aaea2a2a67f0e6e6ec11cfc9c8701b2466ca1c4089 + languageName: node + linkType: hard + "@types/ws@npm:*, @types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" @@ -22919,51 +22829,51 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^8.17.0": - version: 8.54.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" + version: 8.57.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.57.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/type-utils": "npm:8.54.0" - "@typescript-eslint/utils": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.57.0" + "@typescript-eslint/type-utils": "npm:8.57.0" + "@typescript-eslint/utils": "npm:8.57.0" + "@typescript-eslint/visitor-keys": "npm:8.57.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^8.54.0 - eslint: ^8.57.0 || ^9.0.0 + "@typescript-eslint/parser": ^8.57.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/8f1c74ac77d7a84ae3f201bb09cb67271662befed036266af1eaa0653d09b545353441640516c1c86e0a94939887d32f0473c61a642488b14d46533742bfbd1b + checksum: 10/515ed019b16ff2ed4dacb1c2f1cd94227f16f93a8fe086d2bd60f78e6a36ffb20a048d55ddafdac4359d88d16f727c31b36814dba7479c4998f6ad0cc1da2c77 languageName: node linkType: hard "@typescript-eslint/parser@npm:^8.16.0": - version: 8.54.0 - resolution: "@typescript-eslint/parser@npm:8.54.0" + version: 8.57.0 + resolution: "@typescript-eslint/parser@npm:8.57.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.57.0" + "@typescript-eslint/types": "npm:8.57.0" + "@typescript-eslint/typescript-estree": "npm:8.57.0" + "@typescript-eslint/visitor-keys": "npm:8.57.0" debug: "npm:^4.4.3" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/d2e09462c9966ef3deeba71d9e41d1d4876c61eea65888c93a3db6fba48b89a2165459c6519741d40e969da05ed98d3f4c87a7f56c5521ab5699743cc315f6cb + checksum: 10/9f51f8d8a81475d9870f380d9d737b9b59d89a0b7c8f9dce87e23b566d2b95986980717104dc87e2aa207de7ea0880f83963675fbe703c5531016dcacbc4c389 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/project-service@npm:8.54.0" +"@typescript-eslint/project-service@npm:8.57.0": + version: 8.57.0 + resolution: "@typescript-eslint/project-service@npm:8.57.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.54.0" - "@typescript-eslint/types": "npm:^8.54.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.57.0" + "@typescript-eslint/types": "npm:^8.57.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/93f0483f6bbcf7cf776a53a130f7606f597fba67cf111e1897873bf1531efaa96e4851cfd461da0f0cc93afbdb51e47bcce11cf7dd4fb68b7030c7f9f240b92f + checksum: 10/4333c1ac52490926c780b2556d903b3d679d280e60b425d38ae851efa457ebe65b8aa9e1e88651e035527926a368cb52099f4bc395de7ec70f848430576c5db4 languageName: node linkType: hard @@ -22977,38 +22887,38 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.54.0, @typescript-eslint/scope-manager@npm:^8.51.0": - version: 8.54.0 - resolution: "@typescript-eslint/scope-manager@npm:8.54.0" +"@typescript-eslint/scope-manager@npm:8.57.0, @typescript-eslint/scope-manager@npm:^8.56.0": + version: 8.57.0 + resolution: "@typescript-eslint/scope-manager@npm:8.57.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" - checksum: 10/3474f3197e8647754393dee62b3145c9de71eaa66c8a68f61c8283aa332141803885db9c96caa6a51f78128ad9ef92f774a90361655e57bd951d5b57eb76f914 + "@typescript-eslint/types": "npm:8.57.0" + "@typescript-eslint/visitor-keys": "npm:8.57.0" + checksum: 10/72a7086b1605f55dea36909d74e21b023ebd438b393e6ceb736ecc711f487d0add6d4f3648c1fc6c1a01faecd2a7a1f8839f92d8e7fa27f3937000f1fece2e33 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" +"@typescript-eslint/tsconfig-utils@npm:8.57.0, @typescript-eslint/tsconfig-utils@npm:^8.57.0": + version: 8.57.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.57.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/e9d6b29538716f007919bfcee94f09b7f8e7d2b684ad43d1a3c8d43afb9f0539c7707f84a34f42054e31c8c056b0ccf06575d89e860b4d34632ffefaefafe1fc + checksum: 10/cd451a0d1b19faa16314986bcb5aeb4bd98a77f23d4d627304434fc423689a675d6c009f19316006cdca4b83183951fcd8b56d721e595bb6b0d9d52ad0f43c5b languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/type-utils@npm:8.54.0" +"@typescript-eslint/type-utils@npm:8.57.0": + version: 8.57.0 + resolution: "@typescript-eslint/type-utils@npm:8.57.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" - "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.57.0" + "@typescript-eslint/typescript-estree": "npm:8.57.0" + "@typescript-eslint/utils": "npm:8.57.0" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.4.0" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/60e92fb32274abd70165ce6f4187e4cffa55416374c63731d7de8fdcfb7a558b4dd48909ff1ad38ac39d2ea1248ec54d6ce38dbc065fd34529a217fc2450d5b1 + checksum: 10/7ee7ca9090b973f77754e83aebf80c8263f02150109b844ccebb8f5db130b90b95af38343e875ade23fc520a197754107f3706fa0432ae2c32a32e95f1399350 languageName: node linkType: hard @@ -23019,10 +22929,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/types@npm:8.54.0" - checksum: 10/c25cc0bdf90fb150cf6ce498897f43fe3adf9e872562159118f34bd91a9bfab5f720cb1a41f3cdf253b2e840145d7d372089b7cef5156624ef31e98d34f91b31 +"@typescript-eslint/types@npm:8.57.0, @typescript-eslint/types@npm:^8.57.0": + version: 8.57.0 + resolution: "@typescript-eslint/types@npm:8.57.0" + checksum: 10/ba23a4deeb5a89b9b99fee35f58d662901f236000d0f6bcada5143a2ef5ec831c7909e9192def8a48d18f8c3327b78bf3e9c02d770b4a4d721a0422b97ca1e29 languageName: node linkType: hard @@ -23045,37 +22955,37 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.54.0, @typescript-eslint/typescript-estree@npm:^8.23.0": - version: 8.54.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" +"@typescript-eslint/typescript-estree@npm:8.57.0, @typescript-eslint/typescript-estree@npm:^8.23.0": + version: 8.57.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.57.0" dependencies: - "@typescript-eslint/project-service": "npm:8.54.0" - "@typescript-eslint/tsconfig-utils": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/project-service": "npm:8.57.0" + "@typescript-eslint/tsconfig-utils": "npm:8.57.0" + "@typescript-eslint/types": "npm:8.57.0" + "@typescript-eslint/visitor-keys": "npm:8.57.0" debug: "npm:^4.4.3" - minimatch: "npm:^9.0.5" + minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" tinyglobby: "npm:^0.2.15" ts-api-utils: "npm:^2.4.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/3a545037c6f9319251d3ba44cf7a3216b1372422469e27f7ed3415244ebf42553da1ab4644da42d3f0ae2706a8cad12529ffebcb2e75406f74e3b30b812d010d + checksum: 10/eae6027de9b8e0d5c443ad77219689c59dd02085867ea34c0613c93d625cbb9c517fe514fcc38061d49bd39422ca1f170764473b21db178e1db39deeeca6458b languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.48.0, @typescript-eslint/utils@npm:^8.51.0": - version: 8.54.0 - resolution: "@typescript-eslint/utils@npm:8.54.0" +"@typescript-eslint/utils@npm:8.57.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.48.0, @typescript-eslint/utils@npm:^8.56.0": + version: 8.57.0 + resolution: "@typescript-eslint/utils@npm:8.57.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.57.0" + "@typescript-eslint/types": "npm:8.57.0" + "@typescript-eslint/typescript-estree": "npm:8.57.0" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/9f88a2a7ab3e11aa0ff7f99c0e66a0cf2cba10b640def4c64a4f4ef427fecfb22f28dbe5697535915eb01f6507515ac43e45e0ff384bf82856e3420194d9ffdd + checksum: 10/76e3c8eb9f6e28e4cf1359a1b32facaa7523464baeeba8f00a8d68a5a40b3d5d79cfffe48e85d365b06637b6ea6474f63f08a5b5844b2595c2e552e067dc9449 languageName: node linkType: hard @@ -23103,19 +23013,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" +"@typescript-eslint/visitor-keys@npm:8.57.0": + version: 8.57.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.57.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" - eslint-visitor-keys: "npm:^4.2.1" - checksum: 10/cca5380ee30250302ee1459e5a0a38de8c16213026dbbff3d167fa7d71d012f31d60ac4483ad45ebd13f2ac963d1ca52dd5f22759a68d4ee57626e421769187a + "@typescript-eslint/types": "npm:8.57.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10/049edd9e6a5e919bed84bffeefa3d3d944295183feaeb175119c17bcbefa051f10e0e135e4a4dc545c5aa781bd11a276ec5e62fd1211f6692c06a84036b8c4c5 languageName: node linkType: hard -"@uiw/codemirror-extensions-basic-setup@npm:4.25.4": - version: 4.25.4 - resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.4" +"@uiw/codemirror-extensions-basic-setup@npm:4.25.8": + version: 4.25.8 + resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.25.8" dependencies: "@codemirror/autocomplete": "npm:^6.0.0" "@codemirror/commands": "npm:^6.0.0" @@ -23132,19 +23042,19 @@ __metadata: "@codemirror/search": ">=6.0.0" "@codemirror/state": ">=6.0.0" "@codemirror/view": ">=6.0.0" - checksum: 10/8fa523af7cb4ea36db5263221e5a34ecbf18e57ee5c57fbb46f7bfd08ff08523b9fe2c5ae376117d98edaa1a904d1a2966123710e0d866a78ec2d1359955773c + checksum: 10/a8d83465f9f3393b6e95d98ae7f3616ad57f819bce64224831d3db19647524538fc013973074a63551afa69daad9a8ab05f2e5c7441db7e30e722495d7e991d3 languageName: node linkType: hard "@uiw/react-codemirror@npm:^4.9.3": - version: 4.25.4 - resolution: "@uiw/react-codemirror@npm:4.25.4" + version: 4.25.8 + resolution: "@uiw/react-codemirror@npm:4.25.8" dependencies: "@babel/runtime": "npm:^7.18.6" "@codemirror/commands": "npm:^6.1.0" "@codemirror/state": "npm:^6.1.1" "@codemirror/theme-one-dark": "npm:^6.0.0" - "@uiw/codemirror-extensions-basic-setup": "npm:4.25.4" + "@uiw/codemirror-extensions-basic-setup": "npm:4.25.8" codemirror: "npm:^6.0.0" peerDependencies: "@babel/runtime": ">=7.11.0" @@ -23154,7 +23064,7 @@ __metadata: codemirror: ">=6.0.0" react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/679966cfa5cc2e7d5b7c8b40060018bebfc3a2d66492672c64e659e1c719cedd7a10ed7fb3aa014d77c42927581c4ebdf6760ab56568225781798b35dc13b8c6 + checksum: 10/8c974e22dad1ad6231f33f7db42cd5b68caaf9bea545539b06b8a89dda3427eebadf47c8f48ee0d74cdf5a25000a8fcc02bac9fe560b624955eedf1f9bb47a85 languageName: node linkType: hard @@ -23772,42 +23682,44 @@ __metadata: languageName: node linkType: hard -"@whatwg-node/events@npm:^0.1.0": - version: 0.1.1 - resolution: "@whatwg-node/events@npm:0.1.1" - checksum: 10/3a356ca23522190201e27446cfd7ebf1cf96815ddb9d1ba5da0a00bbe6c1d28b4094862104411101fbedd47c758b25fe3683033f6a3e80933029efd664c33567 - languageName: node - linkType: hard - -"@whatwg-node/fetch@npm:^0.9.0": - version: 0.9.14 - resolution: "@whatwg-node/fetch@npm:0.9.14" +"@whatwg-node/disposablestack@npm:^0.0.6": + version: 0.0.6 + resolution: "@whatwg-node/disposablestack@npm:0.0.6" dependencies: - "@whatwg-node/node-fetch": "npm:^0.5.0" - urlpattern-polyfill: "npm:^9.0.0" - checksum: 10/74cdaf82abc2eaa15790fe1a15c8d1208bed090956888c8f35ba622b977e75027edef6b85705b0e7680497f478bd90bf0b784b486de95c84a2806e19d65a1f0c + "@whatwg-node/promise-helpers": "npm:^1.0.0" + tslib: "npm:^2.6.3" + checksum: 10/1b73b7b95a7abad77f81fa30044239da6d5fe636c5c9f259571087437ed093996c484e0e0df00860a37244d7b9e518f7d156161f335040ddda27e7ed2e21ecb2 languageName: node linkType: hard -"@whatwg-node/node-fetch@npm:^0.5.0": - version: 0.5.0 - resolution: "@whatwg-node/node-fetch@npm:0.5.0" +"@whatwg-node/fetch@npm:^0.10.13": + version: 0.10.13 + resolution: "@whatwg-node/fetch@npm:0.10.13" dependencies: - "@whatwg-node/events": "npm:^0.1.0" - busboy: "npm:^1.6.0" - fast-querystring: "npm:^1.1.1" - fast-url-parser: "npm:^1.1.3" - tslib: "npm:^2.3.1" - checksum: 10/b779288b07296e0be60e90e338de46b9d0c892d2e38b99bd04d062bbd4acb429607afc92b9bfdbf10841cef6f4b531e63415197583677de69a07e7b2e39350b9 + "@whatwg-node/node-fetch": "npm:^0.8.3" + urlpattern-polyfill: "npm:^10.0.0" + checksum: 10/23e1dd0242962fa5d253a07ddf37fd4df31a5065d65c0e74edb11616415adb1456df76f9df9eeca28ed7454f0bf39304af5bae33c66d8b05399de4a7779675e8 languageName: node linkType: hard -"@whatwg-node/promise-helpers@npm:^1.0.0": - version: 1.3.0 - resolution: "@whatwg-node/promise-helpers@npm:1.3.0" +"@whatwg-node/node-fetch@npm:^0.8.3": + version: 0.8.5 + resolution: "@whatwg-node/node-fetch@npm:0.8.5" + dependencies: + "@fastify/busboy": "npm:^3.1.1" + "@whatwg-node/disposablestack": "npm:^0.0.6" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + tslib: "npm:^2.6.3" + checksum: 10/6ba6c4ff8af66487a17abcc1b3cc6a6fc5b03268af6211db53da5585f7360924c09a19fb9814de33dde8dafa98708b367538c772fe7102c15804f1bace297ef1 + languageName: node + linkType: hard + +"@whatwg-node/promise-helpers@npm:^1.0.0, @whatwg-node/promise-helpers@npm:^1.2.1, @whatwg-node/promise-helpers@npm:^1.2.4, @whatwg-node/promise-helpers@npm:^1.3.2": + version: 1.3.2 + resolution: "@whatwg-node/promise-helpers@npm:1.3.2" dependencies: tslib: "npm:^2.6.3" - checksum: 10/4971319c340f4c16755dd3fe84772e28bcc550e1f723ae10f94e02910ded545f88e65da0a44499912e23c27ed5f7458e1303a99cdd5818792c5958205ee386b4 + checksum: 10/22513e7075d2e6e067399f6b3065a1f280d77aab2cc8699fe5bf9496a76ea7ede2cf4d46fad6f033d0ad686f974c52f85335c3dcddd656d1c8700636713f94a9 languageName: node linkType: hard @@ -23929,45 +23841,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/core@npm:^2.4.0": - version: 2.4.0 - resolution: "@yarnpkg/core@npm:2.4.0" - dependencies: - "@arcanis/slice-ansi": "npm:^1.0.2" - "@types/semver": "npm:^7.1.0" - "@types/treeify": "npm:^1.0.0" - "@yarnpkg/fslib": "npm:^2.4.0" - "@yarnpkg/json-proxy": "npm:^2.1.0" - "@yarnpkg/libzip": "npm:^2.2.1" - "@yarnpkg/parsers": "npm:^2.3.0" - "@yarnpkg/pnp": "npm:^2.3.2" - "@yarnpkg/shell": "npm:^2.4.1" - binjumper: "npm:^0.1.4" - camelcase: "npm:^5.3.1" - chalk: "npm:^3.0.0" - ci-info: "npm:^2.0.0" - clipanion: "npm:^2.6.2" - cross-spawn: "npm:7.0.3" - diff: "npm:^4.0.1" - globby: "npm:^11.0.1" - got: "npm:^11.7.0" - json-file-plus: "npm:^3.3.1" - lodash: "npm:^4.17.15" - micromatch: "npm:^4.0.2" - mkdirp: "npm:^0.5.1" - p-limit: "npm:^2.2.0" - pluralize: "npm:^7.0.0" - pretty-bytes: "npm:^5.1.0" - semver: "npm:^7.1.2" - stream-to-promise: "npm:^2.2.0" - tar-stream: "npm:^2.0.1" - treeify: "npm:^1.1.0" - tslib: "npm:^1.13.0" - tunnel: "npm:^0.0.6" - checksum: 10/a963ddc09afdfd7dd40acf89bdffb0f6d134240fa2e9bd46dc1bcbe5501b96b7d65b6f30f36354386de097e0a43ea54f4dc4317ce75e2d5b3d6329e4ab0def67 - languageName: node - linkType: hard - "@yarnpkg/core@npm:^4.2.1, @yarnpkg/core@npm:^4.4.0, @yarnpkg/core@npm:^4.4.1": version: 4.4.1 resolution: "@yarnpkg/core@npm:4.4.1" @@ -24011,16 +23884,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/fslib@npm:^2.4.0, @yarnpkg/fslib@npm:^2.5.0": - version: 2.10.4 - resolution: "@yarnpkg/fslib@npm:2.10.4" - dependencies: - "@yarnpkg/libzip": "npm:^2.3.0" - tslib: "npm:^1.13.0" - checksum: 10/c683b91a17138806f11db83af6e6aefd71f485570008effcd68cc39bf84e4243c0072c2a11d613c8289926bcd460e012b9476dd89e61018e21103bc3f42917ca - languageName: node - linkType: hard - "@yarnpkg/fslib@npm:^3.1.2": version: 3.1.2 resolution: "@yarnpkg/fslib@npm:3.1.2" @@ -24030,16 +23893,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/json-proxy@npm:^2.1.0": - version: 2.1.1 - resolution: "@yarnpkg/json-proxy@npm:2.1.1" - dependencies: - "@yarnpkg/fslib": "npm:^2.5.0" - tslib: "npm:^1.13.0" - checksum: 10/22f41ac5c3ee201132c6519da88252d5eea7eda96f554cabb1cdc4b7ff951f3b30f727b8abf457a91b2c8a4d2e7679101347e0beb606350cb4d524fea1159e60 - languageName: node - linkType: hard - "@yarnpkg/libui@npm:^3.0.2": version: 3.0.2 resolution: "@yarnpkg/libui@npm:3.0.2" @@ -24052,16 +23905,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/libzip@npm:^2.2.1, @yarnpkg/libzip@npm:^2.3.0": - version: 2.3.0 - resolution: "@yarnpkg/libzip@npm:2.3.0" - dependencies: - "@types/emscripten": "npm:^1.39.6" - tslib: "npm:^1.13.0" - checksum: 10/0eb147f39eab2830c29120d17e8bfba5aa15dedb940a7378070c67d4de08e9ba8d34068522e15e6b4db94ecaed4ad520e1e517588a36a348d1aa160bc36156ea - languageName: node - linkType: hard - "@yarnpkg/libzip@npm:^3.1.1, @yarnpkg/libzip@npm:^3.2.0, @yarnpkg/libzip@npm:^3.2.1": version: 3.2.1 resolution: "@yarnpkg/libzip@npm:3.2.1" @@ -24093,16 +23936,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/parsers@npm:^2.3.0": - version: 2.6.0 - resolution: "@yarnpkg/parsers@npm:2.6.0" - dependencies: - js-yaml: "npm:^3.10.0" - tslib: "npm:^1.13.0" - checksum: 10/da2c22ce1271383af817b91286fd7532ca8d597a405005e777cb53e702bb7cf688b0b4637c3161351e4e76be43dba0694873cc7845cb9494b9060ddafc5bac3c - languageName: node - linkType: hard - "@yarnpkg/parsers@npm:^3.0.0, @yarnpkg/parsers@npm:^3.0.3": version: 3.0.3 resolution: "@yarnpkg/parsers@npm:3.0.3" @@ -24513,17 +24346,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/pnp@npm:^2.3.2": - version: 2.3.2 - resolution: "@yarnpkg/pnp@npm:2.3.2" - dependencies: - "@types/node": "npm:^13.7.0" - "@yarnpkg/fslib": "npm:^2.4.0" - tslib: "npm:^1.13.0" - checksum: 10/be736c950e888e115a50043e684326fb965ce3ba946dada4a7657faf7a2858afef6b5166a366f095b9498ced114325ae3e0341d9cea83a575938e8a8859e74ef - languageName: node - linkType: hard - "@yarnpkg/pnp@npm:^4.0.9, @yarnpkg/pnp@npm:^4.1.0": version: 4.1.0 resolution: "@yarnpkg/pnp@npm:4.1.0" @@ -24534,24 +24356,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/shell@npm:^2.4.1": - version: 2.4.1 - resolution: "@yarnpkg/shell@npm:2.4.1" - dependencies: - "@yarnpkg/fslib": "npm:^2.4.0" - "@yarnpkg/parsers": "npm:^2.3.0" - clipanion: "npm:^2.6.2" - cross-spawn: "npm:7.0.3" - fast-glob: "npm:^3.2.2" - micromatch: "npm:^4.0.2" - stream-buffers: "npm:^3.0.2" - tslib: "npm:^1.13.0" - bin: - shell: ./lib/cli.js - checksum: 10/ae7c07561ba4ec968b73385bbd4a9ed01a4f30b52f4fc1b46725dcbca29447e221e1c81ed1f94a6e1527397705e95f51489d3696be45a208a88c00d4c33b1da0 - languageName: node - linkType: hard - "@yarnpkg/shell@npm:^4.1.2": version: 4.1.2 resolution: "@yarnpkg/shell@npm:4.1.2" @@ -24684,12 +24488,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" bin: acorn: bin/acorn - checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 + checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b languageName: node linkType: hard @@ -24983,7 +24787,7 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:*, ansi-regex@npm:^6.0.1": +"ansi-regex@npm:*, ansi-regex@npm:^6.0.1, ansi-regex@npm:^6.2.2": version: 6.2.2 resolution: "ansi-regex@npm:6.2.2" checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f @@ -25029,14 +24833,14 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": + version: 6.2.3 + resolution: "ansi-styles@npm:6.2.3" + checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 languageName: node linkType: hard -"any-promise@npm:^1.0.0, any-promise@npm:^1.1.0, any-promise@npm:~1.3.0": +"any-promise@npm:^1.0.0, any-promise@npm:^1.1.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb @@ -25191,12 +24995,12 @@ __metadata: languageName: node linkType: hard -"aria-hidden@npm:^1.1.1": - version: 1.2.3 - resolution: "aria-hidden@npm:1.2.3" +"aria-hidden@npm:^1.2.4": + version: 1.2.6 + resolution: "aria-hidden@npm:1.2.6" dependencies: tslib: "npm:^2.0.0" - checksum: 10/cd7f8474f1bef2dadce8fc74ef6d0fa8c9a477ee3c9e49fc3698e5e93a62014140c520266ee28969d63b5ab474144fe48b6182d010feb6a223f7a73928e6660a + checksum: 10/1914e5a36225dccdb29f0b88cc891eeca736cdc5b0c905ab1437b90b28b5286263ed3a221c75b7dc788f25b942367be0044b2ac8ccf073a72e07a50b1d964202 languageName: node linkType: hard @@ -25380,7 +25184,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:^2.0.0, asap@npm:^2.0.3, asap@npm:~2.0.3, asap@npm:~2.0.6": +"asap@npm:^2.0.0, asap@npm:^2.0.3, asap@npm:~2.0.3": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda @@ -25706,25 +25510,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.13.2": - version: 1.13.2 - resolution: "axios@npm:1.13.2" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.4" - proxy-from-env: "npm:^1.1.0" - checksum: 10/ae4e06dcd18289f2fd18179256d550d27f9a53ecb2f9c59f2ccc4efd1d7151839ba8c3e0fb533dac793e4a59a576ca8689a19244dce5c396680837674a47a867 - languageName: node - linkType: hard - -"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4": - version: 1.13.5 - resolution: "axios@npm:1.13.5" +"axios@npm:^1.0.0, axios@npm:^1.12.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.13.5, axios@npm:^1.13.6, axios@npm:^1.7.4": + version: 1.13.6 + resolution: "axios@npm:1.13.6" dependencies: follow-redirects: "npm:^1.15.11" form-data: "npm:^4.0.5" proxy-from-env: "npm:^1.1.0" - checksum: 10/db726d09902565ef9a0632893530028310e2ec2b95b727114eca1b101450b00014133dfc3871cffc87983fb922bca7e4874d7e2826d1550a377a157cdf3f05b6 + checksum: 10/a7ed83c2af3ef21d64609df0f85e76893a915a864c5934df69241001d0578082d6521a0c730bf37518ee458821b5695957cb10db9fc705f2a8996c8686ea7a89 languageName: node linkType: hard @@ -25903,6 +25696,15 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.2 + resolution: "balanced-match@npm:4.0.2" + dependencies: + jackspeak: "npm:^4.2.3" + checksum: 10/862d6e14832a45558bdd2bc4663ba6d8e7ba60670a6cb1ef952a62348d5f086b16ee71a21b369f0fee808432590c724c823a80cb59eddd140b5ffa3f55497b62 + languageName: node + linkType: hard + "bare-events@npm:^2.5.4, bare-events@npm:^2.7.0": version: 2.8.2 resolution: "bare-events@npm:2.8.2" @@ -26020,9 +25822,9 @@ __metadata: linkType: hard "basic-ftp@npm:^5.0.2": - version: 5.0.3 - resolution: "basic-ftp@npm:5.0.3" - checksum: 10/8f69811a7f4088c5ae39025f1a662522aad517b4065365fbbe80676dc9ccf7a188463dfcb2d9573af2af859d7de70015f612c20a04a311b520efbf7c21dc1ff2 + version: 5.2.0 + resolution: "basic-ftp@npm:5.2.0" + checksum: 10/f5a15d789aa98859af4da9e976154b2aeae19052e1762dc68d259d2bce631dafa40c667aa06d7346cd630aa6f9cc9a26f515b468e0bd24243fbae2149c7d01ad languageName: node linkType: hard @@ -26069,16 +25871,14 @@ __metadata: languageName: node linkType: hard -"bfj@npm:^8.0.0": - version: 8.0.0 - resolution: "bfj@npm:8.0.0" +"bfj@npm:^9.0.2": + version: 9.1.3 + resolution: "bfj@npm:9.1.3" dependencies: - bluebird: "npm:^3.7.2" check-types: "npm:^11.2.3" hoopy: "npm:^0.1.4" - jsonpath: "npm:^1.1.1" tryer: "npm:^1.0.1" - checksum: 10/3e79233e2ba30681a494470d664c654351d2f4fcba7c2972f7e8b6248e374a77a164141164ea32d23f805f0a235aa87dbf480ad0a5939c36f5efbf922de8beb4 + checksum: 10/33119ebc5237345f0032d6d5b1543306298022711891eb28bd7e592663c3a027dec2f561beebd491e6c2f3779883bcaae91df4d958920776e1db9659399e1b8e languageName: node linkType: hard @@ -26149,13 +25949,6 @@ __metadata: languageName: node linkType: hard -"binjumper@npm:^0.1.4": - version: 0.1.4 - resolution: "binjumper@npm:0.1.4" - checksum: 10/9ae6de33ca27b9cc40425227d3d6560ce63f8977855fed70788dc0492f9a048895d79617d8d8152b7b8f66f93d935f25a4bca94cc74d477c3c7cba2c15662dea - languageName: node - linkType: hard - "bintrees@npm:1.0.1": version: 1.0.1 resolution: "bintrees@npm:1.0.1" @@ -26195,9 +25988,9 @@ __metadata: linkType: hard "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10/57ed5a055f946f3e009f1589c45a5242db07f3dddfc72e4506f0dd9d8b145f0dbee4edabc2499288f3fc338eb712fb96a1c623a2ed2bcd49781df1a64db64dd1 languageName: node linkType: hard @@ -26318,6 +26111,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.2 + resolution: "brace-expansion@npm:5.0.2" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/18d382c0919c68f8bb56fbe4a9cb1181a0bf10e6786b5683e586493dfbb517bdcf972f4a3a8d560486627fd9c9c6ecef0a2b8fd454eb6082780307ffd5586251 + languageName: node + linkType: hard + "braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -26924,13 +26726,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 - languageName: node - linkType: hard - "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -27080,7 +26875,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0, ci-info@npm:^3.7.0": +"ci-info@npm:^3.2.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" checksum: 10/b00e9313c1f7042ca8b1297c157c920d6d69f0fbad7b867910235676df228c4b4f4df33d06cacae37f9efba7a160b0a167c6be85492b419ef71d85660e60606b @@ -27120,10 +26915,10 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^2.1.0": - version: 2.1.1 - resolution: "cjs-module-lexer@npm:2.1.1" - checksum: 10/db4331152f4414f3a451865554290eb7d146699aa2aefe1504891951d791ab450f1a75b0477630a4f2942be26f257c4e0627fbfed29d322b5266956b3643f3c1 +"cjs-module-lexer@npm:^2.1.0, cjs-module-lexer@npm:^2.2.0": + version: 2.2.0 + resolution: "cjs-module-lexer@npm:2.2.0" + checksum: 10/fc8eb5c1919504366d8260a150d93c4e857740e770467dc59ca0cc34de4b66c93075559a5af65618f359187866b1be40e036f4e1a1bab2f1e06001c216415f74 languageName: node linkType: hard @@ -27157,6 +26952,16 @@ __metadata: languageName: node linkType: hard +"cleye@npm:^2.3.0": + version: 2.3.0 + resolution: "cleye@npm:2.3.0" + dependencies: + terminal-columns: "npm:^2.0.0" + type-flag: "npm:^4.1.0" + checksum: 10/a6fe9d8431db05907d1699099de338e2a227843ebb85d2b540d609d649dd35b482f6534105daa2930dd18649dd0bfd0526d43c1d746eec10195fda31784953c7 + languageName: node + linkType: hard + "cli-boxes@npm:^2.2.0, cli-boxes@npm:^2.2.1": version: 2.2.1 resolution: "cli-boxes@npm:2.2.1" @@ -27195,7 +27000,7 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.5.0": +"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 @@ -27233,13 +27038,13 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-truncate@npm:4.0.0" +"cli-truncate@npm:^5.0.0": + version: 5.2.0 + resolution: "cli-truncate@npm:5.2.0" dependencies: - slice-ansi: "npm:^5.0.0" - string-width: "npm:^7.0.0" - checksum: 10/d5149175fd25ca985731bdeec46a55ec237475cf74c1a5e103baea696aceb45e372ac4acbaabf1316f06bd62e348123060f8191ffadfeedebd2a70a2a7fb199d + slice-ansi: "npm:^8.0.0" + string-width: "npm:^8.2.0" + checksum: 10/b789b6c2caff1560259aedeb6aaafcf41167d478df418d718a8c92edd6bc5a0ece272b8fb7e7911fbd31cef7b1ac8a30f2b21d90c3174b55a018fe3f2604a137 languageName: node linkType: hard @@ -27264,13 +27069,6 @@ __metadata: languageName: node linkType: hard -"clipanion@npm:^2.6.2": - version: 2.6.2 - resolution: "clipanion@npm:2.6.2" - checksum: 10/f87ca32dd41a7e7898e72f425590c267818c81717c33ea52270354a3f9232a4c4d4f38a5acc0c4b52cb9f9b67962dcf3d326cd57ec2cc3d4345292f0b84e025b - languageName: node - linkType: hard - "clipanion@npm:^4.0.0-rc.2": version: 4.0.0-rc.3 resolution: "clipanion@npm:4.0.0-rc.3" @@ -27430,17 +27228,17 @@ __metadata: languageName: node linkType: hard -"codemirror-graphql@npm:^2.0.10, codemirror-graphql@npm:^2.0.13": - version: 2.0.13 - resolution: "codemirror-graphql@npm:2.0.13" +"codemirror-graphql@npm:^2.2.1": + version: 2.2.4 + resolution: "codemirror-graphql@npm:2.2.4" dependencies: "@types/codemirror": "npm:^0.0.90" - graphql-language-service: "npm:5.2.2" + graphql-language-service: "npm:5.5.0" peerDependencies: "@codemirror/language": 6.0.0 codemirror: ^5.65.3 - graphql: ^15.5.0 || ^16.0.0 - checksum: 10/1a759c3b549c5fa24fffaeb8799156bea0a37c5bf672b09af1a672c89bc4464409ead99e79189644e8c3b4b9746472ef9d5b9f6387cc3fa2f16bfb2c4b75d4db + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + checksum: 10/89e8994588c6145578b707694509854ccdff40c19e82d15dff832c152cadc8978f3688b804b8e75f656d3b7dfa5dc5e6344e854eda5141f0d92c0a505fda1371 languageName: node linkType: hard @@ -27619,13 +27417,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:*, commander@npm:^13.1.0": - version: 13.1.0 - resolution: "commander@npm:13.1.0" - checksum: 10/d3b4b79e6be8471ddadacbb8cd441fe82154d7da7393b50e76165a9e29ccdb74fa911a186437b9a211d0fc071db6051915c94fb8ef16d77511d898e9dbabc6af - languageName: node - linkType: hard - "commander@npm:11.1.0, commander@npm:^11.0.0": version: 11.1.0 resolution: "commander@npm:11.1.0" @@ -27668,6 +27459,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^14.0.3": + version: 14.0.3 + resolution: "commander@npm:14.0.3" + checksum: 10/dfa9ebe2a433d277de5cb0252d23b10a543d245d892db858d23b516336a835c50fd4f52bee4cd13c705cc8acb6f03dc632c73dd806f7d06d3353eb09953dd17a + languageName: node + linkType: hard + "commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -27934,6 +27732,17 @@ __metadata: languageName: node linkType: hard +"constant-case@npm:^3.0.4": + version: 3.0.4 + resolution: "constant-case@npm:3.0.4" + dependencies: + no-case: "npm:^3.0.4" + tslib: "npm:^2.0.3" + upper-case: "npm:^2.0.2" + checksum: 10/6c3346d51afc28d9fae922e966c68eb77a19d94858dba230dd92d7b918b37d36db50f0311e9ecf6847e43e934b1c01406a0936973376ab17ec2c471fbcfb2cf3 + languageName: node + linkType: hard + "constants-browserify@npm:^1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" @@ -28326,17 +28135,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" - dependencies: - path-key: "npm:^3.1.0" - shebang-command: "npm:^2.0.0" - which: "npm:^2.0.1" - checksum: 10/e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce - languageName: node - linkType: hard - "cross-spawn@npm:^6.0.0": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" @@ -28849,7 +28647,7 @@ __metadata: languageName: node linkType: hard -"dataloader@npm:^2.0.0, dataloader@npm:^2.2.2": +"dataloader@npm:^2.0.0, dataloader@npm:^2.2.3": version: 2.2.3 resolution: "dataloader@npm:2.2.3" checksum: 10/83fe6259abe00ae64c5f48252ef59d8e5fcabda9fd4d26685f14a76eeca596bf6f9500d9f22a0094c50c3ea782a0977728f9367e232dfa0fdb5c9d646de279b2 @@ -28918,7 +28716,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:4.4.3, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -29025,7 +28823,7 @@ __metadata: languageName: node linkType: hard -"deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": +"deep-is@npm:^0.1.3": version: 0.1.3 resolution: "deep-is@npm:0.1.3" checksum: 10/dee1094e987a784a9a9c8549fc65eeca3422aef3bf2f9579f76c126085f280311d09273826c2f430d84fd09d64f6a578e5e7a4ac6ba1d50ea6cff0ddf605c025 @@ -29135,21 +28933,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" @@ -29265,17 +29048,17 @@ __metadata: languageName: node linkType: hard -"detect-indent@npm:^7.0.1": - version: 7.0.1 - resolution: "detect-indent@npm:7.0.1" - checksum: 10/cbf3f0b1c3c881934ca94428e1179b26ab2a587e0d719031d37a67fb506d49d067de54ff057cb1e772e75975fed5155c01cd4518306fee60988b1486e3fc7768 +"detect-indent@npm:^7.0.2": + version: 7.0.2 + resolution: "detect-indent@npm:7.0.2" + checksum: 10/ef215d1b55a14f677ce03e840973b25362b6f8cd3f566bc82831fa1abb2be6a95423729bc573dc2334b1371ad7be18d9ec67e1a9611b71a04cb6d63f0d8e54cc languageName: node linkType: hard "detect-libc@npm:^2.0.0": - version: 2.0.3 - resolution: "detect-libc@npm:2.0.3" - checksum: 10/b4ea018d623e077bd395f168a9e81db77370dde36a5b01d067f2ad7989924a81d31cb547ff764acb2aa25d50bb7fdde0b0a93bec02212b0cb430621623246d39 + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 languageName: node linkType: hard @@ -29286,7 +29069,7 @@ __metadata: languageName: node linkType: hard -"detect-newline@npm:^4.0.0": +"detect-newline@npm:^4.0.1": version: 4.0.1 resolution: "detect-newline@npm:4.0.1" checksum: 10/0409ecdfb93419591ccff24fccfe2ddddad29b66637d1ed898872125b25af05014fdeedc9306339577060f69f59fe6e9830cdd80948597f136dfbffefa60599c @@ -29510,12 +29293,12 @@ __metadata: languageName: node linkType: hard -"docker-compose@npm:^1.3.0": - version: 1.3.0 - resolution: "docker-compose@npm:1.3.0" +"docker-compose@npm:^1.3.1": + version: 1.3.1 + resolution: "docker-compose@npm:1.3.1" dependencies: yaml: "npm:^2.2.2" - checksum: 10/7c1b395fc104031eadef08cac0c028af11fd9e3084265dca9e17bc76ad27c2bfe96d8b74df258bb64b198f9e0f1792a7f2c63123cddc0d60bb776f211d54cfb2 + checksum: 10/39e0e4b96c0dea8c88df9770a4d102a9e2437ad0af5f5f28a9f88fbf164eb4fb81fde0d8740500bd451abafab74072e5d5cfa35cc85254722bbe2b7dbc7c033a languageName: node linkType: hard @@ -29663,15 +29446,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.1.7, dompurify@npm:^3.2.4": - version: 3.3.1 - resolution: "dompurify@npm:3.3.1" +"dompurify@npm:^3.1.7, dompurify@npm:^3.3.2": + version: 3.3.2 + resolution: "dompurify@npm:3.3.2" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/f71cca489e628591165d16e8cf4fa4f0d3e2ee48db4d73e9d2c5bedc6f915c92f9e9f101f8c4ba790bec0cdffe7f4e1747f5e31c69dc53ce7ae20a81ff6b0022 + checksum: 10/3ca02559677ce6d9583a500f21ffbb6b9e88f1af99f69fa0d0d9442cddbac98810588c869f8b435addb5115492d6e49870024bca322169b941bafedb99c7f281 languageName: node linkType: hard @@ -29739,7 +29522,7 @@ __metadata: languageName: node linkType: hard -"dset@npm:^3.1.2, dset@npm:^3.1.4": +"dset@npm:^3.1.2": version: 3.1.4 resolution: "dset@npm:3.1.4" checksum: 10/6268c9e2049c8effe6e5a1952f02826e8e32468b5ced781f15f8f3b1c290da37626246fec014fbdd1503413f981dff6abd8a4c718ec9952fd45fccb6ac9de43f @@ -29816,7 +29599,7 @@ __metadata: "@types/fs-extra": "npm:^11.0.0" "@types/node": "npm:^22.13.14" chalk: "npm:^4.0.0" - commander: "npm:^12.0.0" + commander: "npm:^14.0.3" cross-fetch: "npm:^4.0.0" fs-extra: "npm:^11.2.0" handlebars: "npm:^4.7.3" @@ -30005,22 +29788,13 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:~1.1.0": - version: 1.1.0 - resolution: "end-of-stream@npm:1.1.0" - dependencies: - once: "npm:~1.3.0" - checksum: 10/9fa637e259e50e5e3634e8e14064a183bd0d407733594631362f9df596409739bef5f7064840e6725212a9edc8b4a70a5a3088ac423e8564f9dc183dd098c719 - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.17.4, enhanced-resolve@npm:^5.18.0": - version: 5.19.0 - resolution: "enhanced-resolve@npm:5.19.0" +"enhanced-resolve@npm:^5.18.0, enhanced-resolve@npm:^5.20.0": + version: 5.20.0 + resolution: "enhanced-resolve@npm:5.20.0" dependencies: graceful-fs: "npm:^4.2.4" tapable: "npm:^2.3.0" - checksum: 10/b537d52173bf1ba903c623f96a43ea3b51466ee2b606b2fcca30d73d453fd79c8683dccbb83523de27cd02763c906f11486e2591a4335e6afe49fa5ad6e67b83 + checksum: 10/ba22699e4b46dc1be6441c359636ebcdd5028229219a7d6ba10f39996401f950967f8297ddf3284d0ee8e33c8133a8742696154e383cc08d8bd2bf80ba87df97 languageName: node linkType: hard @@ -30055,13 +29829,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:~2.1.0": - version: 2.1.0 - resolution: "entities@npm:2.1.0" - checksum: 10/fe71642e42e108540b0324dea03e00f3dbad93617c601bfcf292c3f852c236af3e58469219c4653f6f05df781a446f3b82105b8d26b936d0fa246b0103f2f951 - languageName: node - linkType: hard - "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -30515,25 +30282,6 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^1.8.1": - version: 1.14.3 - resolution: "escodegen@npm:1.14.3" - dependencies: - esprima: "npm:^4.0.1" - estraverse: "npm:^4.2.0" - esutils: "npm:^2.0.2" - optionator: "npm:^0.8.1" - source-map: "npm:~0.6.1" - dependenciesMeta: - source-map: - optional: true - bin: - escodegen: bin/escodegen.js - esgenerate: bin/esgenerate.js - checksum: 10/70f095ca9393535f9f1c145ef99dc0b3ff14cca6bc4a79d90ff3352f90c3f2e07f75af6d6c05174ea67c45271f75e80dd440dd7d04ed2cf44c9452c3042fa84a - languageName: node - linkType: hard - "escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -30686,11 +30434,13 @@ __metadata: linkType: hard "eslint-plugin-node-import@npm:^1.0.5": - version: 1.0.5 - resolution: "eslint-plugin-node-import@npm:1.0.5" + version: 1.2.0 + resolution: "eslint-plugin-node-import@npm:1.2.0" + dependencies: + globals: "npm:^17.3.0" peerDependencies: eslint: ">=7" - checksum: 10/d77c03871c3397d09db18d5242030b8490769077a488d4267c6c3f38ceffb9b86d162b32c8d5fbafe92ff4bc7668a1a5971f2020e6edab38c37b0a2e6155a591 + checksum: 10/bfd36ad4e8d74cc0da900151c19cb40328e62218e730055327d7c35b53af103e37d414701b7de2269b1eabcc8fafab0972e98a48489ac9f367e39618138aeecd languageName: node linkType: hard @@ -30745,55 +30495,54 @@ __metadata: linkType: hard "eslint-plugin-storybook@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "eslint-plugin-storybook@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "eslint-plugin-storybook@npm:10.3.0-alpha.14" dependencies: "@typescript-eslint/utils": "npm:^8.48.0" peerDependencies: eslint: ">=8" - storybook: ^10.3.0-alpha.1 - checksum: 10/2b3b6c11709c536b9639f7ed91bfb0cb5035129f0cbb245abd59f208ce179a0f99b558e3f8e89987a83d80ab30395dfc0560f401295123edf221cafba7a05c1a + storybook: ^10.3.0-alpha.14 + checksum: 10/851c3912503ff014aac3e0d964574fbecae381b5f77239a07e0b036e2ae1c258d3e279c8e82bbb0c9f286be57883aa2d78c5330e3da3c6719ad4805d8e330b70 languageName: node linkType: hard "eslint-plugin-testing-library@npm:^7.0.0": - version: 7.15.4 - resolution: "eslint-plugin-testing-library@npm:7.15.4" + version: 7.16.0 + resolution: "eslint-plugin-testing-library@npm:7.16.0" dependencies: - "@typescript-eslint/scope-manager": "npm:^8.51.0" - "@typescript-eslint/utils": "npm:^8.51.0" + "@typescript-eslint/scope-manager": "npm:^8.56.0" + "@typescript-eslint/utils": "npm:^8.56.0" peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - checksum: 10/be717d041ce6d368f7dd422813f620f455aff6c7c61a9364287af32fe18d31ca88467c2bf50e742bf470820db5f7bea617aac232c8e0a61daadcee8b76428786 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + checksum: 10/6581b41d25838c61b818f6b80e998ff2d72a646531c1c017023842fe5523a9ea3e9e75e7ce0d84abee9d73309602d64911985cbaace8dfbb0a94415da1b213a8 languageName: node linkType: hard "eslint-plugin-unused-imports@npm:^4.1.4": - version: 4.3.0 - resolution: "eslint-plugin-unused-imports@npm:4.3.0" + version: 4.4.1 + resolution: "eslint-plugin-unused-imports@npm:4.4.1" peerDependencies: "@typescript-eslint/eslint-plugin": ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 - eslint: ^9.0.0 || ^8.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true - checksum: 10/64ec1f686d90f18a27c27273a0338bad6964a611f497b81c5e7eace9d9a4f38d96070d7c389c2626095a9e5e7dbcafccc6444fa2933c9a7649996a7ca875738f + checksum: 10/b420fd55c393a6fdacfdbd0d1adf4cd44bed9a6584f05245091a6716272c57f38154d04b76f253619d8bf22823c0b9d630ef6b5b09edad6e51b8a1f7aec56c22 languageName: node linkType: hard "eslint-rspack-plugin@npm:^4.2.1": - version: 4.3.0 - resolution: "eslint-rspack-plugin@npm:4.3.0" + version: 4.4.1 + resolution: "eslint-rspack-plugin@npm:4.4.1" dependencies: "@types/eslint": "npm:^8.56.10" jest-worker: "npm:^29.7.0" micromatch: "npm:^4.0.8" normalize-path: "npm:^3.0.0" - schema-utils: "npm:^4.2.0" tinyglobby: "npm:^0.2.15" peerDependencies: - eslint: ^8.0.0 || ^9.0.0 - checksum: 10/e312c993474cd4df180f6790359936b581391ac660769ce94b7e00e96322aedbbebc8d9132969db72b04d4588ccf40dd17c45d8f5d48df625a015a819c215ae3 + eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 + checksum: 10/310bddfd2818d885d382e481b510e68648fff735ba1bbf93cb03f191d92c109baca4a0072c4fa00c28347fbba03aabfe890075d2e2cade557ab9e3d5feb7a149 languageName: node linkType: hard @@ -30824,10 +30573,10 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.1": - version: 4.2.1 - resolution: "eslint-visitor-keys@npm:4.2.1" - checksum: 10/3ee00fc6a7002d4b0ffd9dc99e13a6a7882c557329e6c25ab254220d71e5c9c4f89dca4695352949ea678eb1f3ba912a18ef8aac0a7fe094196fd92f441bfce2 +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec languageName: node linkType: hard @@ -30932,16 +30681,6 @@ __metadata: languageName: node linkType: hard -"esprima@npm:1.2.2": - version: 1.2.2 - resolution: "esprima@npm:1.2.2" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10/7ed9274abb1fed861354239f0da8fa3ec6592617ad0fd2aab16b0beb0425137c46f05c82faa0de89b3bb3d704054815c2657658c7e221b1fb550e88c237eefd2 - languageName: node - linkType: hard - "esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -30970,7 +30709,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": +"estraverse@npm:^4.1.1": version: 4.3.0 resolution: "estraverse@npm:4.3.0" checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb @@ -31163,7 +30902,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" - "@types/jquery": "npm:^3.3.34" + "@types/jquery": "npm:^4.0.0" "@types/react": "npm:*" "@types/react-dom": "npm:*" "@types/zen-observable": "npm:^0.8.0" @@ -31244,7 +30983,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" - "@types/jquery": "npm:^3.3.34" + "@types/jquery": "npm:^4.0.0" "@types/react": "npm:*" "@types/react-dom": "npm:*" "@types/zen-observable": "npm:^0.8.0" @@ -31300,7 +31039,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-signals-backend": "workspace:^" "@backstage/plugin-techdocs-backend": "workspace:^" - "@opentelemetry/auto-instrumentations-node": "npm:^0.67.0" + "@opentelemetry/auto-instrumentations-node": "npm:^0.71.0" "@opentelemetry/exporter-prometheus": "npm:^0.211.0" "@opentelemetry/sdk-node": "npm:^0.211.0" example-app: "link:../app" @@ -31339,23 +31078,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:^8.0.1": - version: 8.0.1 - resolution: "execa@npm:8.0.1" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^8.0.1" - human-signals: "npm:^5.0.0" - is-stream: "npm:^3.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^5.1.0" - onetime: "npm:^6.0.0" - signal-exit: "npm:^4.1.0" - strip-final-newline: "npm:^3.0.0" - checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d - languageName: node - linkType: hard - "exit-hook@npm:^2.2.1": version: 2.2.1 resolution: "exit-hook@npm:2.2.1" @@ -31455,23 +31177,14 @@ __metadata: languageName: node linkType: hard -"express-rate-limit@npm:^7.5.0": - version: 7.5.1 - resolution: "express-rate-limit@npm:7.5.1" - peerDependencies: - express: ">= 4.11" - checksum: 10/357c3398450144ab7bbce2841d0bf4f93a0f3fd9d1d5ed9a0ee331b557af969cc790941dc37b47f8d9b5672964aa0e31666f770e1f48b334dc7d1e69f6433040 - languageName: node - linkType: hard - -"express-rate-limit@npm:^8.2.1": - version: 8.2.1 - resolution: "express-rate-limit@npm:8.2.1" +"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" dependencies: - ip-address: "npm:10.0.1" + ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/7cbf70df2e88e590e463d2d8f93380775b2ea181d97f2c50c2ff9f2c666c247f83109a852b21d9c99ccc5762119101f281f54a27252a2f1a0a918be6d71f955b + checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf languageName: node linkType: hard @@ -31589,13 +31302,6 @@ __metadata: languageName: node linkType: hard -"extract-files@npm:^11.0.0": - version: 11.0.0 - resolution: "extract-files@npm:11.0.0" - checksum: 10/02bf0dde9617d67795e38a182d8bf58828a7c5d77762623ff05e72d461a0e980071a860e2503231db2cc8824d8da35cefb1750937dcbe018cb0e67e37f20a7be - languageName: node - linkType: hard - "fast-copy@npm:^3.0.2": version: 3.0.2 resolution: "fast-copy@npm:3.0.2" @@ -31658,7 +31364,7 @@ __metadata: languageName: node linkType: hard -"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": +"fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" checksum: 10/eb7e220ecf2bab5159d157350b81d01f75726a4382f5a9266f42b9150c4523b9795f7f5d9fbbbeaeac09a441b2369f05ee02db48ea938584205530fe5693cfe1 @@ -31672,7 +31378,7 @@ __metadata: languageName: node linkType: hard -"fast-querystring@npm:^1.0.0, fast-querystring@npm:^1.1.1": +"fast-querystring@npm:^1.0.0": version: 1.1.2 resolution: "fast-querystring@npm:1.1.2" dependencies: @@ -31716,45 +31422,33 @@ __metadata: languageName: node linkType: hard -"fast-url-parser@npm:^1.1.3": - version: 1.1.3 - resolution: "fast-url-parser@npm:1.1.3" - dependencies: - punycode: "npm:^1.3.2" - checksum: 10/6d33f46ce9776f7f3017576926207a950ca39bc5eb78fc794404f2288fe494720f9a119084b75569bd9eb09d2b46678bfaf39c191fb2c808ef3c833dc8982752 +"fast-xml-builder@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-xml-builder@npm:1.0.0" + checksum: 10/06c04d80545e5c9f4d1d6cca00567b5cc09953a92c6328fa48cfb4d7f42630313b8c2bb62e9cb81accee7bb5e1c5312fcae06c3d20dbe52d969a5938233316da languageName: node linkType: hard -"fast-xml-parser@npm:4.4.1, fast-xml-parser@npm:^4.4.1": - version: 4.4.1 - resolution: "fast-xml-parser@npm:4.4.1" +"fast-xml-parser@npm:5.4.1, fast-xml-parser@npm:^5.3.4": + version: 5.4.1 + resolution: "fast-xml-parser@npm:5.4.1" + dependencies: + fast-xml-builder: "npm:^1.0.0" + strnum: "npm:^2.1.2" + bin: + fxparser: src/cli/cli.js + checksum: 10/2b40067c3ad3542ca197d1353bcb0416cd5db20d5c66d74ac176b99af6ff9bd55a6182d36856a2fd477c95b8fc1f07405475f1662a31185480130ba7076c702a + languageName: node + linkType: hard + +"fast-xml-parser@npm:^4.4.1": + version: 4.5.4 + resolution: "fast-xml-parser@npm:4.5.4" dependencies: strnum: "npm:^1.0.5" bin: fxparser: src/cli/cli.js - checksum: 10/0c05ab8703630d8c857fafadbd78d0020d3a8e54310c3842179cd4a0d9d97e96d209ce885e91241f4aa9dd8dfc2fd924a682741a423d65153cad34da2032ec44 - languageName: node - linkType: hard - -"fast-xml-parser@npm:5.2.5": - version: 5.2.5 - resolution: "fast-xml-parser@npm:5.2.5" - dependencies: - strnum: "npm:^2.1.0" - bin: - fxparser: src/cli/cli.js - checksum: 10/305017cff6968a34cbac597317be1516e85c44f650f30d982c84f8c30043e81fd38d39a8810d570136c921399dd43b9ac4775bdfbbbcfee96456f3c086b48bdd - languageName: node - linkType: hard - -"fast-xml-parser@npm:^5.3.4": - version: 5.3.5 - resolution: "fast-xml-parser@npm:5.3.5" - dependencies: - strnum: "npm:^2.1.2" - bin: - fxparser: src/cli/cli.js - checksum: 10/913363c2cf9ab8038bd2b666698d99d44b977725f0198f3dfff3a5d34c3109ef49d3a163a0f390f69ed00ad33b81355112dec8be5e79a13f8e6c7aaf146204b8 + checksum: 10/991f11a15d82be778c3452e5f1109975d66276bb951ba4db87417507da15d0b1c09d15a4e4db15a216cf3315b4325f66ff3b7f9b7557d6a2055103755fb39cce languageName: node linkType: hard @@ -31870,7 +31564,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.0.0": +"figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" dependencies: @@ -32068,15 +31762,15 @@ __metadata: linkType: hard "find-process@npm:^2.0.0": - version: 2.0.0 - resolution: "find-process@npm:2.0.0" + version: 2.1.0 + resolution: "find-process@npm:2.1.0" dependencies: chalk: "npm:~4.1.2" commander: "npm:^12.1.0" loglevel: "npm:^1.9.2" bin: - find-process: dist/bin/find-process.js - checksum: 10/a8f23b72e7b67c8f039c2234116f68439c067e8d0f90484b53814aac04f1891cf322c7cf4b4193a14b91a19ba9c68bd8bcfb8a556626e6667dcaaefbca3294f9 + find-process: dist/cjs/bin/find-process.js + checksum: 10/5ccaaddf66eacf74eb2605ddd8d8aaa4515029dc78eed2b0a39ceef3a4f1ae229046d30ff44845fd496b6306e2c750a7abad8db16f0683be7c32aa502dc61a21 languageName: node linkType: hard @@ -32159,10 +31853,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:3.3.3, flatted@npm:^3.1.0, flatted@npm:^3.2.7": - version: 3.3.3 - resolution: "flatted@npm:3.3.3" - checksum: 10/8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe +"flatted@npm:^3.1.0, flatted@npm:^3.2.7, flatted@npm:^3.3.4": + version: 3.4.1 + resolution: "flatted@npm:3.4.1" + checksum: 10/39a308e2ef82d2d8c80ebc74b67d4ff3f668be168137b649440b6735eb9c115d1e0c13ab0d9958b3d2ea9c85087ab7e14c14aa6f625a22b2916d89bbd91bc4a0 languageName: node linkType: hard @@ -32192,7 +31886,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11, follow-redirects@npm:^1.15.6": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11": version: 1.15.11 resolution: "follow-redirects@npm:1.15.11" peerDependenciesMeta: @@ -32397,12 +32091,12 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^12.31.0": - version: 12.31.0 - resolution: "framer-motion@npm:12.31.0" +"framer-motion@npm:^12.38.0": + version: 12.38.0 + resolution: "framer-motion@npm:12.38.0" dependencies: - motion-dom: "npm:^12.30.1" - motion-utils: "npm:^12.29.2" + motion-dom: "npm:^12.38.0" + motion-utils: "npm:^12.36.0" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -32415,7 +32109,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/8cd76953b5e4e81e69b7bbec699cd5c913df87897148cd0f9fe85fa2f1c4e2768fbaeb6e40be9cf6d3f4b17a5a8024351d7e0ba93a9cdf9d8358c5031c5cdecd + checksum: 10/4d529d1648a8e31ec9859e7ff1296b7e4ef0028eb09cbc7d626068766ab53e486038b431fac33b1438a1cc076a244e6843c5a8c0f38442885832308452b4b25e languageName: node linkType: hard @@ -32480,14 +32174,14 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:11.3.3, fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0, fs-extra@npm:~11.3.0": - version: 11.3.3 - resolution: "fs-extra@npm:11.3.3" +"fs-extra@npm:11.3.4, fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0, fs-extra@npm:~11.3.0": + version: 11.3.4 + resolution: "fs-extra@npm:11.3.4" dependencies: graceful-fs: "npm:^4.2.0" jsonfile: "npm:^6.0.1" universalify: "npm:^2.0.0" - checksum: 10/daeaefafbebe8fa6efd2fb96fc926f2c952be5877811f00a6794f0d64e0128e3d0d93368cd328f8f063b45deacf385c40e3d931aa46014245431cd2f4f89c67a + checksum: 10/1b8deea9c540a2efe63c750bc9e1ba6238115579d1571d67fe8fb58e3fb6df19aba29fd4ebb81217cf0bf5bce0df30ca68dbc3e06f6652b856edd385ce0ff649 languageName: node linkType: hard @@ -32707,7 +32401,7 @@ __metadata: languageName: node linkType: hard -"gcp-metadata@npm:^6.0.0, gcp-metadata@npm:^6.1.0": +"gcp-metadata@npm:^6.1.0": version: 6.1.0 resolution: "gcp-metadata@npm:6.1.0" dependencies: @@ -32784,10 +32478,10 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0": - version: 1.2.0 - resolution: "get-east-asian-width@npm:1.2.0" - checksum: 10/c9b280e7c7c67fb89fa17e867c4a9d1c9f1321aba2a9ee27bff37fb6ca9552bccda328c70a80c1f83a0e39ba1b7e3427e60f47823402d19e7a41b83417ec047a +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": + version: 1.5.0 + resolution: "get-east-asian-width@npm:1.5.0" + checksum: 10/60bc34cd1e975055ab99f0f177e31bed3e516ff7cee9c536474383954a976abaa6b94a51d99ad158ef1e372790fa096cab7d07f166bb0778f6587954c0fbe946 languageName: node linkType: hard @@ -32857,13 +32551,6 @@ __metadata: languageName: node linkType: hard -"get-stdin@npm:^9.0.0": - version: 9.0.0 - resolution: "get-stdin@npm:9.0.0" - checksum: 10/5972bc34d05932b45512c8e2d67b040f1c1ca8afb95c56cbc480985f2d761b7e37fe90dc8abd22527f062cc5639a6930ff346e9952ae4c11a2d4275869459594 - languageName: node - linkType: hard - "get-stream@npm:^4.0.0, get-stream@npm:^4.1.0": version: 4.1.0 resolution: "get-stream@npm:4.1.0" @@ -32889,13 +32576,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^8.0.1": - version: 8.0.1 - resolution: "get-stream@npm:8.0.1" - checksum: 10/dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e - languageName: node - linkType: hard - "get-stream@npm:^9.0.1": version: 9.0.1 resolution: "get-stream@npm:9.0.1" @@ -32938,6 +32618,15 @@ __metadata: languageName: node linkType: hard +"get-value@npm:^3.0.1": + version: 3.0.1 + resolution: "get-value@npm:3.0.1" + dependencies: + isobject: "npm:^3.0.1" + checksum: 10/3ba777d33448181d8b6c21ce11f31194257119d32dbd632b27db6e3f27fe78100405b4dd93137eea9f4aa2d0a9c623b13747d728d472a7ca6cb93046c3f521a1 + languageName: node + linkType: hard + "getopts@npm:2.3.0": version: 2.3.0 resolution: "getopts@npm:2.3.0" @@ -32945,10 +32634,10 @@ __metadata: languageName: node linkType: hard -"git-hooks-list@npm:^3.0.0": - version: 3.1.0 - resolution: "git-hooks-list@npm:3.1.0" - checksum: 10/05cbdb29e1e14f3b6fde78c876a34383e4476b1be32e8486ad03293f01add884c1a8df8c2dce2ca5d99119c94951b2ff9fa9cbd51d834ae6477b6813cefb998f +"git-hooks-list@npm:^4.1.1": + version: 4.2.1 + resolution: "git-hooks-list@npm:4.2.1" + checksum: 10/39449520045539c03b1d45d3a010424849152d6f723b638c6f19cb8c8b0993bb30ed5ef09653f4d1c1356f5fb51a396059096d8bdfcb6ca7ccb54f6e26efc338 languageName: node linkType: hard @@ -33014,7 +32703,7 @@ __metadata: languageName: node linkType: hard -"glob-to-regex.js@npm:^1.0.1": +"glob-to-regex.js@npm:^1.0.0, glob-to-regex.js@npm:^1.0.1": version: 1.2.0 resolution: "glob-to-regex.js@npm:1.2.0" peerDependencies: @@ -33030,14 +32719,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:13.0.0": - version: 13.0.0 - resolution: "glob@npm:13.0.0" +"glob@npm:13.0.6, glob@npm:^13.0.1": + version: 13.0.6 + resolution: "glob@npm:13.0.6" dependencies: - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 languageName: node linkType: hard @@ -33057,7 +32746,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.1, glob@npm:^11.1.0": +"glob@npm:^11.0.1": version: 11.1.0 resolution: "glob@npm:11.1.0" dependencies: @@ -33176,10 +32865,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^15.11.0": - version: 15.15.0 - resolution: "globals@npm:15.15.0" - checksum: 10/7f561c87b2fd381b27fc2db7df8a4ea7a9bb378667b8a7193e61fd2ca3a876479174e2a303a74345fbea6e1242e16db48915c1fd3bf35adcf4060a795b425e18 +"globals@npm:^17.0.0, globals@npm:^17.3.0": + version: 17.4.0 + resolution: "globals@npm:17.4.0" + checksum: 10/ffad244617e94efcb3da72b7beefc941167c21316148ce378f322db7af72db06468f370e23224b3c7b17b5173a7c75b134e5e7b0949f2828519054a76892508d languageName: node linkType: hard @@ -33214,20 +32903,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" @@ -33397,35 +33072,33 @@ __metadata: languageName: node linkType: hard -"graphiql@npm:3.1.1": - version: 3.1.1 - resolution: "graphiql@npm:3.1.1" +"graphiql@npm:^3.9.0": + version: 3.9.0 + resolution: "graphiql@npm:3.9.0" dependencies: - "@graphiql/react": "npm:^0.20.3" - "@graphiql/toolkit": "npm:^0.9.1" - graphql-language-service: "npm:^5.2.0" - markdown-it: "npm:^12.2.0" + "@graphiql/react": "npm:^0.29.0" + react-compiler-runtime: "npm:19.1.0-rc.1" peerDependencies: - graphql: ^15.5.0 || ^16.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 - checksum: 10/c234f24e3bb568f210a45f6a408cc12adb2a82cc7be8ecbdf81cadc7ae398f35474a443fbc20e99d6125acf20268e6a666b05b928725542ea3d133c51798687f + checksum: 10/33a17064594fc883262de07e8cd3012563cf5965e401a8ab3e1aed9c38e69b4222a3cd1cefa58e3c0e5ba9e01dcd403d0452c4991814702da4d6fb37acc9a8ae languageName: node linkType: hard -"graphql-config@npm:^5.0.2": - version: 5.1.5 - resolution: "graphql-config@npm:5.1.5" +"graphql-config@npm:^5.1.6": + version: 5.1.6 + resolution: "graphql-config@npm:5.1.6" dependencies: "@graphql-tools/graphql-file-loader": "npm:^8.0.0" "@graphql-tools/json-file-loader": "npm:^8.0.0" "@graphql-tools/load": "npm:^8.1.0" "@graphql-tools/merge": "npm:^9.0.0" - "@graphql-tools/url-loader": "npm:^8.0.0" - "@graphql-tools/utils": "npm:^10.0.0" + "@graphql-tools/url-loader": "npm:^9.0.0" + "@graphql-tools/utils": "npm:^11.0.0" cosmiconfig: "npm:^8.1.0" jiti: "npm:^2.0.0" - minimatch: "npm:^9.0.5" + minimatch: "npm:^10.0.0" string-env-interpolation: "npm:^1.0.1" tslib: "npm:^2.4.0" peerDependencies: @@ -33434,7 +33107,7 @@ __metadata: peerDependenciesMeta: cosmiconfig-toml-loader: optional: true - checksum: 10/f33a4e73265f84790888d05d7dbf50a0b3454adf6e184ac018165e679ecab7fca746fbf4069dea601261f193dcb5a015a1679403ea1fd4eab109e79d8fed306d + checksum: 10/941b3166d395d22ae6f4048c564b09364d2234c88f8ad596188599d4650c28a35a2484b549e0ef239ae461a23874efc69e0234f7f8977a8a6f39640867086003 languageName: node linkType: hard @@ -33447,18 +33120,18 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:5.2.2, graphql-language-service@npm:^5.2.0, graphql-language-service@npm:^5.2.2": - version: 5.2.2 - resolution: "graphql-language-service@npm:5.2.2" +"graphql-language-service@npm:5.5.0, graphql-language-service@npm:^5.3.1": + version: 5.5.0 + resolution: "graphql-language-service@npm:5.5.0" dependencies: debounce-promise: "npm:^3.1.2" nullthrows: "npm:^1.0.0" vscode-languageserver-types: "npm:^3.17.1" peerDependencies: - graphql: ^15.5.0 || ^16.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 bin: graphql: dist/temp-bin.js - checksum: 10/3148b0a49fb2784eaf363a2d7e4fe0800f69c11d24528c2daaff1bf3d07be24db61e1d84b6bec94c68068f7ad1fc6d459edfbda964418ad3735bc353a636a690 + checksum: 10/7f398395cab50faaad717d107413df5424ac102bf33ac4a5e4ef34bf7c2e13a27d51fd93456601f560874801bf93bec281589164d901bab3aef56f4518782f60 languageName: node linkType: hard @@ -33484,7 +33157,7 @@ __metadata: languageName: node linkType: hard -"graphql-ws@npm:^5.14.0, graphql-ws@npm:^5.4.1": +"graphql-ws@npm:^5.4.1": version: 5.16.2 resolution: "graphql-ws@npm:5.16.2" peerDependencies: @@ -33493,6 +33166,25 @@ __metadata: languageName: node linkType: hard +"graphql-ws@npm:^6.0.6": + version: 6.0.7 + resolution: "graphql-ws@npm:6.0.7" + peerDependencies: + "@fastify/websocket": ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + ws: ^8 + peerDependenciesMeta: + "@fastify/websocket": + optional: true + crossws: + optional: true + ws: + optional: true + checksum: 10/633b142a7a8683f900f1f3590a30ff696076d94d17cc8c0a42d069288cd8ca77b4967e87a9f7ac884c026106be42055b9e2bda5e7de28579bd2fc8e65d0b0424 + languageName: node + linkType: hard + "graphql@npm:^14.0.2 || ^15.5": version: 15.10.1 resolution: "graphql@npm:15.10.1" @@ -33501,9 +33193,9 @@ __metadata: linkType: hard "graphql@npm:^16.0.0, graphql@npm:^16.8.1": - version: 16.12.0 - resolution: "graphql@npm:16.12.0" - checksum: 10/e299bc97cca178e549c8c1ed4cb164f631f07be987d3657f76cdf18c0250040cc0d456d4b6d41c87b855cac97b15a62ed345557527efcb0546492895a893bb87 + version: 16.13.1 + resolution: "graphql@npm:16.13.1" + checksum: 10/a42f857f60351e1f4665aa5bc5524796d3f45bf81793e6db932f902aff769b0488dafaa1d9c07bddda7122a1f6d0b0105fab12d38992f6b6fb81fc3d7ced1afc languageName: node linkType: hard @@ -33704,7 +33396,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.2": +"hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -33938,9 +33630,9 @@ __metadata: linkType: hard "hono@npm:^4.11.4": - version: 4.11.7 - resolution: "hono@npm:4.11.7" - checksum: 10/16f5a715f70430bd4050b250207adf7c567774c1d91386d5454577fbc191fc4a50b912628845ce8392fae0e3fd9f364a947412961e3747a9f0b2f714790b738e + version: 4.12.7 + resolution: "hono@npm:4.12.7" + checksum: 10/fed37e612730491ba9456f8f68f1b8727a5298cd839fff9641a0b7a95b1e8567a05abb819d32621b40988f166b01140cf7d573c9218dee2741004f48e09564d5 languageName: node linkType: hard @@ -34331,13 +34023,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^5.0.0": - version: 5.0.0 - resolution: "human-signals@npm:5.0.0" - checksum: 10/30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 - languageName: node - linkType: hard - "humanize-duration@npm:^3.25.1": version: 3.33.2 resolution: "humanize-duration@npm:3.33.2" @@ -34479,7 +34164,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 @@ -34508,9 +34193,9 @@ __metadata: linkType: hard "immutable@npm:^3.x.x": - version: 3.8.2 - resolution: "immutable@npm:3.8.2" - checksum: 10/8a94647c769e97c9685be1b89d5e1b3171e8c1361fb9061fbcf78f630f70bf60e4de0bfca8bdd24a54b1fb814a945a76a30b11b7ee08967f9802a138a54498a2 + version: 3.8.3 + resolution: "immutable@npm:3.8.3" + checksum: 10/29db366d4dff83b0b296150c351b08db24837005909a71c123d860c1d2a40605f19a3ecd56d1e96be9799e947ddf7906897a28fa2e81f0b8c63c652b6bfe5550 languageName: node linkType: hard @@ -34554,6 +34239,18 @@ __metadata: languageName: node linkType: hard +"import-in-the-middle@npm:^3.0.0": + version: 3.0.0 + resolution: "import-in-the-middle@npm:3.0.0" + dependencies: + acorn: "npm:^8.15.0" + acorn-import-attributes: "npm:^1.9.5" + cjs-module-lexer: "npm:^2.2.0" + module-details-from-path: "npm:^1.0.4" + checksum: 10/0bf1f22d00a080e7f651db8c5d136aa4aca6829397769f22fb544a67d9117b1c78590f180a690eb19eb0cfb4a558beac66b6508d346cccc91e1e5f75c934e9de + languageName: node + linkType: hard + "import-lazy@npm:^2.1.0": version: 2.1.0 resolution: "import-lazy@npm:2.1.0" @@ -34718,7 +34415,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:8.2.7, inquirer@npm:^8.0.0, inquirer@npm:^8.2.0": +"inquirer@npm:^8.0.0, inquirer@npm:^8.2.0": version: 8.2.7 resolution: "inquirer@npm:8.2.7" dependencies: @@ -34788,7 +34485,7 @@ __metadata: languageName: node linkType: hard -"invariant@npm:^2.2.2, invariant@npm:^2.2.4": +"invariant@npm:^2.2.2": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -34814,10 +34511,10 @@ __metadata: languageName: node linkType: hard -"ip-address@npm:10.0.1": - version: 10.0.1 - resolution: "ip-address@npm:10.0.1" - checksum: 10/09731acda32cd8e14c46830c137e7e5940f47b36d63ffb87c737331270287d631cf25aa95570907a67d3f919fdb25f4470c404eda21e62f22e0a55927f4dd0fb +"ip-address@npm:10.1.0": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10/a6979629d1ad9c1fb424bc25182203fad739b40225aebc55ec6243bbff5035faf7b9ed6efab3a097de6e713acbbfde944baacfa73e11852bb43989c45a68d79e languageName: node linkType: hard @@ -34845,10 +34542,10 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.1.0": - version: 2.2.0 - resolution: "ipaddr.js@npm:2.2.0" - checksum: 10/9e1cdd9110b3bca5d910ab70d7fb1933e9c485d9b92cb14ef39f30c412ba3fe02a553921bf696efc7149cc653453c48ccf173adb996ec27d925f1f340f872986 +"ipaddr.js@npm:^2.1.0, ipaddr.js@npm:^2.3.0": + version: 2.3.0 + resolution: "ipaddr.js@npm:2.3.0" + checksum: 10/be3d01bc2e20fc2dc5349b489ea40883954b816ce3e57aa48ad943d4e7c4ace501f28a7a15bde4b96b6b97d0fbb28d599ff2f87399f3cda7bd728889402eed3b languageName: node linkType: hard @@ -34974,7 +34671,7 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": +"is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 @@ -35103,19 +34800,12 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^4.0.0": - version: 4.0.0 - resolution: "is-fullwidth-code-point@npm:4.0.0" - checksum: 10/8ae89bf5057bdf4f57b346fb6c55e9c3dd2549983d54191d722d5c739397a903012cc41a04ee3403fd872e811243ef91a7c5196da7b5841dc6b6aae31a264a8d - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^5.0.0": - version: 5.0.0 - resolution: "is-fullwidth-code-point@npm:5.0.0" +"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0": + version: 5.1.0 + resolution: "is-fullwidth-code-point@npm:5.1.0" dependencies: - get-east-asian-width: "npm:^1.0.0" - checksum: 10/8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 + get-east-asian-width: "npm:^1.3.1" + checksum: 10/4700d8a82cb71bd2a2955587b2823c36dc4660eadd4047bfbd070821ddbce8504fc5f9b28725567ecddf405b1e06c6692c9b719f65df6af9ec5262bc11393a6a languageName: node linkType: hard @@ -35283,13 +34973,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" @@ -35297,13 +34980,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" @@ -35473,13 +35149,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "is-stream@npm:3.0.0" - checksum: 10/172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 - languageName: node - linkType: hard - "is-stream@npm:^4.0.1": version: 4.0.1 resolution: "is-stream@npm:4.0.1" @@ -35619,13 +35288,6 @@ __metadata: languageName: node linkType: hard -"is@npm:^3.2.1, is@npm:^3.3.0": - version: 3.3.0 - resolution: "is@npm:3.3.0" - checksum: 10/f77dc5a05a1e8fd1f1de282add9bb01c44dae27af72b883bf0ce342151dec48f125b0b8923efa78c1e93c4fb866095629b2c7de3e5e3853aea4ed17c82c5cd8d - languageName: node - linkType: hard - "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -35697,8 +35359,8 @@ __metadata: linkType: hard "isomorphic-git@npm:^1.23.0": - version: 1.36.3 - resolution: "isomorphic-git@npm:1.36.3" + version: 1.37.2 + resolution: "isomorphic-git@npm:1.37.2" dependencies: async-lock: "npm:^1.4.1" clean-git-ref: "npm:^2.0.1" @@ -35713,7 +35375,7 @@ __metadata: simple-get: "npm:^4.0.1" bin: isogit: cli.cjs - checksum: 10/b6295326773ed3ba4b0f7abb7470c65f11de8b9e19ff0b8e58d85575491a461fae600a579e5de91c15398144eefec3f95c99082ac2c866abbc66e0dd225273c8 + checksum: 10/23c225821588b7e85bc5f7eb975ad2e909a0dc954241ee019cfd18c826d3d1119804d719e2105f41212284f81b74bd907696f59e47b68ddad2fbde83859ab3a2 languageName: node linkType: hard @@ -35751,6 +35413,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:^1.0.7": + version: 1.0.7 + resolution: "isows@npm:1.0.7" + peerDependencies: + ws: "*" + checksum: 10/044b949b369872882af07b60b613b5801ae01b01a23b5b72b78af80c8103bbeed38352c3e8ceff13a7834bc91fd2eb41cf91ec01d59a041d8705680e6b0ec546 + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-lib-coverage@npm:3.2.0" @@ -35861,12 +35532,12 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.1.1": - version: 4.1.1 - resolution: "jackspeak@npm:4.1.1" +"jackspeak@npm:^4.1.1, jackspeak@npm:^4.2.3": + version: 4.2.3 + resolution: "jackspeak@npm:4.2.3" dependencies: - "@isaacs/cliui": "npm:^8.0.2" - checksum: 10/ffceb270ec286841f48413bfb4a50b188662dfd599378ce142b6540f3f0a66821dc9dcb1e9ebc55c6c3b24dc2226c96e5819ba9bd7a241bd29031b61911718c7 + "@isaacs/cliui": "npm:^9.0.0" + checksum: 10/b88e3fe5fa04d34f0f939a15b7cef4a8589999b7a366ef89a3e0f2c45d2a7666066b67cbf46d57c3a4796a76d27b9d869b23d96a803dd834200d222c2a70de7e languageName: node linkType: hard @@ -36692,19 +36363,6 @@ __metadata: languageName: node linkType: hard -"json-file-plus@npm:^3.3.1": - version: 3.3.1 - resolution: "json-file-plus@npm:3.3.1" - dependencies: - is: "npm:^3.2.1" - node.extend: "npm:^2.0.0" - object.assign: "npm:^4.1.0" - promiseback: "npm:^2.0.2" - safer-buffer: "npm:^2.0.2" - checksum: 10/6b71dad39e0fd8d0a23a82ca70b7c94adfcd59986e63165935d2adba5502076b75f3267e357372dd118f9d680ecc142f0f67617de9f27139c3c8b113cdd9c574 - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -36871,9 +36529,9 @@ __metadata: linkType: hard "jsonc-parser@npm:^3.2.0": - version: 3.2.0 - resolution: "jsonc-parser@npm:3.2.0" - checksum: 10/bd68b902e5f9394f01da97921f49c5084b2dc03a0c5b4fdb2a429f8d6f292686c1bf87badaeb0a8148d024192a88f5ad2e57b2918ba43fe25cf15f3371db64d4 + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10/9b0dc391f20b47378f843ef1e877e73ec652a5bdc3c5fa1f36af0f119a55091d147a86c1ee86a232296f55c929bba174538c2bf0312610e0817a22de131cc3f4 languageName: node linkType: hard @@ -36924,8 +36582,8 @@ __metadata: linkType: hard "jsonpath-plus@npm:^10.0.0, jsonpath-plus@npm:^10.3.0, jsonpath-plus@npm:^6.0.1 || ^10.1.0": - version: 10.3.0 - resolution: "jsonpath-plus@npm:10.3.0" + version: 10.4.0 + resolution: "jsonpath-plus@npm:10.4.0" dependencies: "@jsep-plugin/assignment": "npm:^1.3.0" "@jsep-plugin/regex": "npm:^1.0.4" @@ -36933,18 +36591,7 @@ __metadata: bin: jsonpath: bin/jsonpath-cli.js jsonpath-plus: bin/jsonpath-cli.js - checksum: 10/082302334414c7c5ab0cc8239563118f7f14bb2949d001b009f436491d00f94a7a293eed3eaf61ffdaf72f6fda9d25198a4280c4f68a4c403154ca7ed2bd0dc9 - languageName: node - linkType: hard - -"jsonpath@npm:^1.1.1": - version: 1.1.1 - resolution: "jsonpath@npm:1.1.1" - dependencies: - esprima: "npm:1.2.2" - static-eval: "npm:2.0.2" - underscore: "npm:1.12.1" - checksum: 10/aa6c2fea9c05eeba4a37870cbbcf30c20de5211d0fd967786b6c59b8546c9f80182328ee2428daf989c8d5c6e6bf97fed28eefc790144258b1238707c30706eb + checksum: 10/0ff33c7eb6500d7c8d789ce15a63ac2c46cb01b855f1c53729ca9e3833e0253af70277fc1799ebfe0b3130ddc03c127562669b999729dd11f2621b81472248d4 languageName: node linkType: hard @@ -37177,6 +36824,17 @@ __metadata: languageName: node linkType: hard +"keytar@npm:^7.9.0": + version: 7.9.0 + resolution: "keytar@npm:7.9.0" + dependencies: + node-addon-api: "npm:^4.3.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.0.1" + checksum: 10/904795bc304f8ad89b80f915c869a941a383312b58584212a199473d18647035cfda92a9c53e6c53bf13ea0fed23037c9597eb418a5c71ee9454f140f026fac9 + languageName: node + linkType: hard + "keyv@npm:*, keyv@npm:^5.2.1": version: 5.6.0 resolution: "keyv@npm:5.6.0" @@ -37275,20 +36933,21 @@ __metadata: linkType: hard "knip@npm:^5.42.0": - version: 5.83.1 - resolution: "knip@npm:5.83.1" + version: 5.86.0 + resolution: "knip@npm:5.86.0" dependencies: "@nodelib/fs.walk": "npm:^1.2.3" fast-glob: "npm:^3.3.3" formatly: "npm:^0.3.0" jiti: "npm:^2.6.0" - js-yaml: "npm:^4.1.1" minimist: "npm:^1.2.8" - oxc-resolver: "npm:^11.15.0" + oxc-resolver: "npm:^11.19.1" picocolors: "npm:^1.1.1" picomatch: "npm:^4.0.1" smol-toml: "npm:^1.5.2" strip-json-comments: "npm:5.0.3" + unbash: "npm:^2.2.0" + yaml: "npm:^2.8.2" zod: "npm:^4.1.11" peerDependencies: "@types/node": ">=18" @@ -37296,7 +36955,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/d4783b22eacbcb726790211cb6e7cfb966cbb42c4737ae71fbe766523add2e9b8c7eb389a14f9418742d7722f26b107b036c6f0e1af4b56d86a7097f62172a54 + checksum: 10/1b8c94f62d2868b0c62499474f462dc09d57e10e73335c01caacff8e0c0b120f441e0881028db30b6a1c185f98a78e2fa727e8faa150bca40c41848263aee04f languageName: node linkType: hard @@ -37397,11 +37056,11 @@ __metadata: linkType: hard "ldapts@npm:^8.0.6": - version: 8.1.6 - resolution: "ldapts@npm:8.1.6" + version: 8.1.7 + resolution: "ldapts@npm:8.1.7" dependencies: strict-event-emitter-types: "npm:2.0.0" - checksum: 10/3ebcdf335e1477d920cdea562fdf1f91218cb4bed6de7077a837a33ec93f3a8e1659e346c28335bd0be0144505315f9445c87acae857b8af385939f0ac78a60e + checksum: 10/6dc0002df51a62de5391066df981fc3dab5471dc733fdedd698b58dea4c802ca20d17498518757555d979abc93a320b3e88bbcf3b380c1635930e48aebf3847c languageName: node linkType: hard @@ -37429,16 +37088,6 @@ __metadata: languageName: node linkType: hard -"levn@npm:~0.3.0": - version: 0.3.0 - resolution: "levn@npm:0.3.0" - dependencies: - prelude-ls: "npm:~1.1.2" - type-check: "npm:~0.3.2" - checksum: 10/e1c3e75b5c430d9aa4c32c83c8a611e4ca53608ca78e3ea3bf6bbd9d017e4776d05d86e27df7901baebd3afa732abede9f26f715b8c1be19e95505c7a3a7b589 - languageName: node - linkType: hard - "libsodium-wrappers@npm:*, libsodium-wrappers@npm:^0.8.0": version: 0.8.2 resolution: "libsodium-wrappers@npm:0.8.2" @@ -37480,13 +37129,6 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^3.1.3": - version: 3.1.3 - resolution: "lilconfig@npm:3.1.3" - checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3 - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -37494,15 +37136,6 @@ __metadata: languageName: node linkType: hard -"linkify-it@npm:^3.0.1": - version: 3.0.2 - resolution: "linkify-it@npm:3.0.2" - dependencies: - uc.micro: "npm:^1.0.1" - checksum: 10/c1fcabd105c2bb97ce993dd9c6474b2f29ab759176f4fddb82fa01f497a0cea268c67276c7bd43a5cf390a6af54e25ead77ea70f75a648fc9736caff6aa6f2f4 - languageName: node - linkType: hard - "linkify-it@npm:^5.0.0": version: 5.0.0 resolution: "linkify-it@npm:5.0.0" @@ -37529,23 +37162,19 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:^15.0.0": - version: 15.5.2 - resolution: "lint-staged@npm:15.5.2" +"lint-staged@npm:^16.0.0": + version: 16.3.3 + resolution: "lint-staged@npm:16.3.3" dependencies: - chalk: "npm:^5.4.1" - commander: "npm:^13.1.0" - debug: "npm:^4.4.0" - execa: "npm:^8.0.1" - lilconfig: "npm:^3.1.3" - listr2: "npm:^8.2.5" + commander: "npm:^14.0.3" + listr2: "npm:^9.0.5" micromatch: "npm:^4.0.8" - pidtree: "npm:^0.6.0" string-argv: "npm:^0.3.2" - yaml: "npm:^2.7.0" + tinyexec: "npm:^1.0.2" + yaml: "npm:^2.8.2" bin: lint-staged: bin/lint-staged.js - checksum: 10/523c332d6cb6e34972a6530a7a2487307555e784df9466c82f2b8d17c8090a3db561a6362065ae6b63048c25fcb85c9e32057cd0bfb756bf7ab185bea1dbb89c + checksum: 10/64b20a0aa8710a89db49b4d4384f44e471de8544133cd9f80b1ebeac148144e43b1553aea2b34bf530fd32b554374ee8128c9332033ba138f1a9685964de1cd3 languageName: node linkType: hard @@ -37556,17 +37185,17 @@ __metadata: languageName: node linkType: hard -"listr2@npm:^8.2.5": - version: 8.2.5 - resolution: "listr2@npm:8.2.5" +"listr2@npm:^9.0.5": + version: 9.0.5 + resolution: "listr2@npm:9.0.5" dependencies: - cli-truncate: "npm:^4.0.0" + cli-truncate: "npm:^5.0.0" colorette: "npm:^2.0.20" eventemitter3: "npm:^5.0.1" log-update: "npm:^6.1.0" rfdc: "npm:^1.4.1" wrap-ansi: "npm:^9.0.0" - checksum: 10/c76542f18306195e464fe10203ee679a7beafa9bf0dc679ebacb416387cca8f5307c1d8ba35483d26ba611dc2fac5a1529733dce28f2660556082fb7eebb79f9 + checksum: 10/b78ffd60443aed9a8e0fc9162eb941ea4d63210700d61a895eb29348f23fc668327e26bbca87a9e6a6208e7fa96c475fe1f1c6c19b46f376f547e0cba3b935ae languageName: node linkType: hard @@ -37956,7 +37585,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.15.0, lodash@npm:^4.16.4, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.15, lodash@npm:~4.17.21, lodash@npm:~4.17.23": +"lodash@npm:^4.15.0, lodash@npm:^4.16.4, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.21, lodash@npm:~4.17.23": version: 4.17.23 resolution: "lodash@npm:4.17.23" checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 @@ -38187,10 +37816,10 @@ __metadata: languageName: node linkType: hard -"lru.min@npm:^1.1.0, lru.min@npm:^1.1.3": - version: 1.1.3 - resolution: "lru.min@npm:1.1.3" - checksum: 10/b741bf51e6f2620f35f66657c31a37bb4e6aaa3d7f4dfd8a657cd4e6a4d75be881057d71f54a907a4938d5993677d92cdfa71203054a73af6978514ba73767ee +"lru.min@npm:^1.1.0, lru.min@npm:^1.1.4": + version: 1.1.4 + resolution: "lru.min@npm:1.1.4" + checksum: 10/c7041fb558fa0c39b3e14b8c711716af729a4df96af25e6a2d54f5d60d936a1e33edbabcfa3a71359f73f746763dc51da3fc679d60e2f087aa722f139f56ccd4 languageName: node linkType: hard @@ -38413,21 +38042,6 @@ __metadata: languageName: node linkType: hard -"markdown-it@npm:^12.2.0": - version: 12.3.2 - resolution: "markdown-it@npm:12.3.2" - dependencies: - argparse: "npm:^2.0.1" - entities: "npm:~2.1.0" - linkify-it: "npm:^3.0.1" - mdurl: "npm:^1.0.1" - uc.micro: "npm:^1.0.5" - bin: - markdown-it: bin/markdown-it.js - checksum: 10/d83d794bfb9f5e05750b25db401d9c1f9b97c6bbabb6cfd78988bb98652c62c24417435487238e2b91fd4e495547ae8c9429fb4c69e9f5bf49bd0dd292d53f24 - languageName: node - linkType: hard - "markdown-it@npm:^14.1.0": version: 14.1.0 resolution: "markdown-it@npm:14.1.0" @@ -38697,7 +38311,7 @@ __metadata: languageName: node linkType: hard -"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1": +"mdurl@npm:^1.0.0": version: 1.0.1 resolution: "mdurl@npm:1.0.1" checksum: 10/ada367d01c9e81d07328101f187d5bd8641b71f33eab075df4caed935a24fa679e625f07108801d8250a5e4a99e5cd4be7679957a11424a3aa3e740d2bb2d5cb @@ -38779,17 +38393,27 @@ __metadata: languageName: node linkType: hard -"memfs@npm:^4.51.1, memfs@npm:^4.6.0": - version: 4.51.1 - resolution: "memfs@npm:4.51.1" +"memfs@npm:^4.56.10, memfs@npm:^4.6.0": + version: 4.56.11 + resolution: "memfs@npm:4.56.11" dependencies: + "@jsonjoy.com/fs-core": "npm:4.56.11" + "@jsonjoy.com/fs-fsa": "npm:4.56.11" + "@jsonjoy.com/fs-node": "npm:4.56.11" + "@jsonjoy.com/fs-node-builtins": "npm:4.56.11" + "@jsonjoy.com/fs-node-to-fsa": "npm:4.56.11" + "@jsonjoy.com/fs-node-utils": "npm:4.56.11" + "@jsonjoy.com/fs-print": "npm:4.56.11" + "@jsonjoy.com/fs-snapshot": "npm:4.56.11" "@jsonjoy.com/json-pack": "npm:^1.11.0" "@jsonjoy.com/util": "npm:^1.9.0" glob-to-regex.js: "npm:^1.0.1" thingies: "npm:^2.5.0" tree-dump: "npm:^1.0.3" tslib: "npm:^2.0.0" - checksum: 10/a2f70cd1b366f910a39bb1a398c03d6f3f0db936376697eca3e176609e9f6acc0ed40fb44d6293dd15eb3f730c3fce536e5024395e3dc92f54374133c96ba1b7 + peerDependencies: + tslib: 2 + checksum: 10/2d99bce59dee0f5c96a039851892e39c09c1eee11d8978cf37dbb93462d706d842d6a5b0b3d5081d47f19883ed4b6d973c8086301a9155b098b80d66c3e79cb7 languageName: node linkType: hard @@ -38851,15 +38475,15 @@ __metadata: languageName: node linkType: hard -"meros@npm:^1.1.4, meros@npm:^1.2.1": - version: 1.3.0 - resolution: "meros@npm:1.3.0" +"meros@npm:^1.1.4, meros@npm:^1.3.2": + version: 1.3.2 + resolution: "meros@npm:1.3.2" peerDependencies: "@types/node": ">=13" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/1893d226866058a32161ab069294a1a16975c765416a2b05165dfafba07cd958ca12503e35c621ffe736c62d935ccb1ce60cb723e2a9e0b85e02bb3236722ef6 + checksum: 10/9269b243f91b714b75169f63231af81bcd4c049c1308f6e78fc08214af89323ce2e36a7c1603cde6f32cf4ba79075365335c0d6b549e997b53124568f213f0a5 languageName: node linkType: hard @@ -39326,13 +38950,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 10/995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 - languageName: node - linkType: hard - "mimic-function@npm:^5.0.0": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" @@ -39369,14 +38986,14 @@ __metadata: linkType: hard "mini-css-extract-plugin@npm:^2.4.2": - version: 2.10.0 - resolution: "mini-css-extract-plugin@npm:2.10.0" + version: 2.10.1 + resolution: "mini-css-extract-plugin@npm:2.10.1" dependencies: schema-utils: "npm:^4.0.0" tapable: "npm:^2.2.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/bae5350ab82171c6c9a22a4397df14aa69280f5ff0e1ff4d2429ea841bc096927b1e27ba7b75a9c3dd77bd44bab449d6197bd748381f1326cbc8befcb10d1a9e + checksum: 10/2d0cecc3bea85cd7f9b1ce0974f1672976d610a9267e2988ff19f5d03b017bff12b32151a412de0f519a70be7d3b050b499b20101445fb21728cc2d35dd4041a languageName: node linkType: hard @@ -39403,16 +39020,16 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:10.0.3": - version: 10.0.3 - resolution: "minimatch@npm:10.0.3" +"minimatch@npm:10.2.1": + version: 10.2.1 + resolution: "minimatch@npm:10.2.1" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8 + brace-expansion: "npm:^5.0.2" + checksum: 10/d41c195ee1f2c70a75641088e36d0fa5fa286cb6fe48558e6d3bf3d95f640eda453c217707215389b12234df12175f65f338c0b841b36b0125177dbd6a80d026 languageName: node linkType: hard -"minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -39421,6 +39038,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:3.1.5, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f + languageName: node + linkType: hard + "minimatch@npm:9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" @@ -39430,12 +39056,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.0.0, minimatch@npm:^10.1.1, minimatch@npm:^10.2.1, minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f languageName: node linkType: hard @@ -39448,16 +39074,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^7.4.3": - version: 7.4.6 - resolution: "minimatch@npm:7.4.6" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/0046ba1161ac6414bde1b07c440792ebcdb2ed93e6714c85c73974332b709b7e692801550bc9da22028a8613407b3f13861e17dd0dd44f4babdeacd44950430b - languageName: node - linkType: hard - -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": +"minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -39606,13 +39223,6 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.0.0": - version: 4.2.8 - resolution: "minipass@npm:4.2.8" - checksum: 10/e148eb6dcb85c980234cad889139ef8ddf9d5bdac534f4f0268446c8792dd4c74f4502479be48de3c1cce2f6450f6da4d0d4a86405a8a12be04c1c36b339569a - languageName: node - linkType: hard - "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" @@ -39620,10 +39230,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 languageName: node linkType: hard @@ -39664,7 +39274,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.5, mkdirp@npm:^0.5.6": +"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.5": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" dependencies: @@ -39684,6 +39294,15 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba + languageName: node + linkType: hard + "mock-socket@npm:^9.3.0": version: 9.3.1 resolution: "mock-socket@npm:9.3.1" @@ -39804,10 +39423,10 @@ __metadata: languageName: node linkType: hard -"module-details-from-path@npm:^1.0.3": - version: 1.0.3 - resolution: "module-details-from-path@npm:1.0.3" - checksum: 10/f93226e9154fc8cb91f4609b639167ec7ad9155b30be4924d9717656648a3ae5f181d4e2338434d4c5afc7b5f4c10dd3b64109e5b89a4be70b20a25ba3573d54 +"module-details-from-path@npm:^1.0.3, module-details-from-path@npm:^1.0.4": + version: 1.0.4 + resolution: "module-details-from-path@npm:1.0.4" + checksum: 10/2ebfada5358492f6ab496b70f70a1042f2ee7a4c79d29467f59ed6704f741fb4461d7cecb5082144ed39a05fec4d19e9ff38b731c76228151be97227240a05b2 languageName: node linkType: hard @@ -39832,27 +39451,27 @@ __metadata: languageName: node linkType: hard -"motion-dom@npm:^12.30.1": - version: 12.30.1 - resolution: "motion-dom@npm:12.30.1" +"motion-dom@npm:^12.38.0": + version: 12.38.0 + resolution: "motion-dom@npm:12.38.0" dependencies: - motion-utils: "npm:^12.29.2" - checksum: 10/22af7e074b388485b8f383bc9a8a3d03dec51f2e7112a3b50b9fde7076531ec0fccbeb61c669a439eb0a535992b121a48e612901b79f139538697755c291400e + motion-utils: "npm:^12.36.0" + checksum: 10/78c040b46d93273932cf80c70e39845be5a442dcaf18d4345b45a9193de9dfa87c885b609943cb652115e4eac5d46ef40b452185073dd43fc328b134f9975e90 languageName: node linkType: hard -"motion-utils@npm:^12.29.2": - version: 12.29.2 - resolution: "motion-utils@npm:12.29.2" - checksum: 10/ae5f9be58c07939af72334894ed1a18653d724946182a718dc3d11268ef26e63804c3f16dee5a6110596d4406b539c4513822b74f86adebef9488601c34b18b7 +"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.31.0 - resolution: "motion@npm:12.31.0" + version: 12.38.0 + resolution: "motion@npm:12.38.0" dependencies: - framer-motion: "npm:^12.31.0" + framer-motion: "npm:^12.38.0" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -39865,7 +39484,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10/abb2253b2e679f3d153e472f63e29fa0f3d9d4dd6bb2f2b0c9026897dd069a2371ddc2374bf70debdd480351ad5311e830ebf98e5ea7a44588e44099381e950b + checksum: 10/d7ae2ba3cc112c4467822956b92065239640b9c62204d3bee1780da9fc0147185373534138d39975e82bf73b5f1b28d3fb3581031e4e7e0cfb230472767bd10d languageName: node linkType: hard @@ -39973,17 +39592,14 @@ __metadata: linkType: hard "multer@npm:^2.0.2": - version: 2.0.2 - resolution: "multer@npm:2.0.2" + version: 2.1.1 + resolution: "multer@npm:2.1.1" dependencies: append-field: "npm:^1.0.0" busboy: "npm:^1.6.0" concat-stream: "npm:^2.0.0" - mkdirp: "npm:^0.5.6" - object-assign: "npm:^4.1.1" type-is: "npm:^1.6.18" - xtend: "npm:^4.0.2" - checksum: 10/4bdcb07138cf72f93adc08a0dc27c058faab9f6721067a58f394fa546d73d11b7f100cdd66e733d649d184f9d1b402065f6888b31ec427409f056ee92c4367a6 + checksum: 10/fb22868caaed37d725715c14c60b740b81665265da3a026bb61954414f65b99f76b360128413b8a2a7cc1a95ecae28a42bf831fe172bb79682d19ec105b556bd languageName: node linkType: hard @@ -40019,6 +39635,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "mute-stream@npm:1.0.0" + checksum: 10/36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 + languageName: node + linkType: hard + "mute-stream@npm:^2.0.0": version: 2.0.0 resolution: "mute-stream@npm:2.0.0" @@ -40027,19 +39650,20 @@ __metadata: linkType: hard "mysql2@npm:^3.0.0": - version: 3.16.2 - resolution: "mysql2@npm:3.16.2" + version: 3.19.0 + resolution: "mysql2@npm:3.19.0" dependencies: aws-ssl-profiles: "npm:^1.1.2" denque: "npm:^2.1.0" generate-function: "npm:^2.3.1" iconv-lite: "npm:^0.7.2" long: "npm:^5.3.2" - lru.min: "npm:^1.1.3" + lru.min: "npm:^1.1.4" named-placeholders: "npm:^1.1.6" - seq-queue: "npm:^0.0.5" - sqlstring: "npm:^2.3.3" - checksum: 10/b44cff80bb804b56eec422cce65555ea3879d8bb1ccc0e1e26df09c8300167a123396e8591da09c1f8769d16d498f9aa454d0b0a11e53235cfd403f0f6597ba2 + sql-escaper: "npm:^1.3.3" + peerDependencies: + "@types/node": ">= 8" + checksum: 10/62d0ac029ed152647d48bebfeceb009acc493544e6358e9c3a0595bce03de764ca0902e3c94b4222d7d02ab40761c12856ff6e2a29c4f1ddc8865e8fbd49652c languageName: node linkType: hard @@ -40276,6 +39900,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^4.3.0": + version: 4.3.0 + resolution: "node-addon-api@npm:4.3.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/d3b38d16cb9ad0714d965331d0e38cef1c27750c2c3343cd3464a9ed8158501a2910ccbf2fd9fdc476e806a19dbc9e0524ff9d66a7c779d42a9752a63ba30b80 + languageName: node + linkType: hard + "node-addon-api@npm:^8.0.0, node-addon-api@npm:^8.2.2, node-addon-api@npm:^8.3.0, node-addon-api@npm:^8.3.1": version: 8.5.0 resolution: "node-addon-api@npm:8.5.0" @@ -40582,16 +40215,6 @@ __metadata: languageName: node linkType: hard -"node.extend@npm:^2.0.0": - version: 2.0.3 - resolution: "node.extend@npm:2.0.3" - dependencies: - hasown: "npm:^2.0.0" - is: "npm:^3.3.0" - checksum: 10/f500ace16d0b90e9db3919676de593eb37e7b82d8d9b67d95a40e5856ef5842592df3364b4d01fc2c3f4c0dea6dd9d627444dd85fe18581b7a22caad5ffab249 - languageName: node - linkType: hard - "nodemailer@npm:^7.0.7": version: 7.0.13 resolution: "nodemailer@npm:7.0.13" @@ -40600,13 +40223,13 @@ __metadata: linkType: hard "nodemon@npm:^3.0.1": - version: 3.1.11 - resolution: "nodemon@npm:3.1.11" + version: 3.1.14 + resolution: "nodemon@npm:3.1.14" dependencies: chokidar: "npm:^3.5.2" debug: "npm:^4" ignore-by-default: "npm:^1.0.1" - minimatch: "npm:^3.1.2" + minimatch: "npm:^10.2.1" pstree.remy: "npm:^1.1.8" semver: "npm:^7.5.3" simple-update-notifier: "npm:^2.0.0" @@ -40615,7 +40238,7 @@ __metadata: undefsafe: "npm:^2.0.5" bin: nodemon: bin/nodemon.js - checksum: 10/0f43d2c70abe0764e26e438dbe0c78bc429746a558dabf5dd94b13f6f3a79b4bd7d5793347acafddaee5eab594a806c2ad43efc999d342e28d185661718da8dc + checksum: 10/187aa93fc461bb001bac5de63df49a38a7b1cb3c60a5fd6a3b0f2b15da68fe5adfd0f91244be2fd1f57263cbd351e3113cb04c4edf4af23699dd95b2782fbf9b languageName: node linkType: hard @@ -40837,15 +40460,6 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^5.1.0": - version: 5.1.0 - resolution: "npm-run-path@npm:5.1.0" - dependencies: - path-key: "npm:^4.0.0" - checksum: 10/dc184eb5ec239d6a2b990b43236845332ef12f4e0beaa9701de724aa797fe40b6bbd0157fb7639d24d3ab13f5d5cf22d223a19c6300846b8126f335f788bee66 - languageName: node - linkType: hard - "npmlog@npm:^5.0.1": version: 5.0.1 resolution: "npmlog@npm:5.0.1" @@ -40977,7 +40591,7 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:^4.1.0, object.assign@npm:^4.1.4, object.assign@npm:^4.1.7": +"object.assign@npm:^4.1.4, object.assign@npm:^4.1.7": version: 4.1.7 resolution: "object.assign@npm:4.1.7" dependencies: @@ -41114,15 +40728,6 @@ __metadata: languageName: node linkType: hard -"once@npm:~1.3.0": - version: 1.3.3 - resolution: "once@npm:1.3.3" - dependencies: - wrappy: "npm:1" - checksum: 10/8e832de08b1d73b470e01690c211cb4fcefccab1fd1bd19e706d572d74d3e9b7e38a8bfcdabdd364f9f868757d9e8e5812a59817dc473eaf698ff3bfae2219f2 - languageName: node - linkType: hard - "one-time@npm:^1.0.0": version: 1.0.0 resolution: "one-time@npm:1.0.0" @@ -41141,15 +40746,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: "npm:^4.0.0" - checksum: 10/0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 - languageName: node - linkType: hard - "onetime@npm:^7.0.0": version: 7.0.0 resolution: "onetime@npm:7.0.0" @@ -41299,20 +40895,6 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.8.1": - version: 0.8.3 - resolution: "optionator@npm:0.8.3" - dependencies: - deep-is: "npm:~0.1.3" - fast-levenshtein: "npm:~2.0.6" - levn: "npm:~0.3.0" - prelude-ls: "npm:~1.1.2" - type-check: "npm:~0.3.2" - word-wrap: "npm:~1.2.3" - checksum: 10/6fa3c841b520f10aec45563962922215180e8cfbc59fde3ecd4ba2644ad66ca96bd19ad0e853f22fefcb7fc10e7612a5215b412cc66c5588f9a3138b38f6b5ff - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -41399,30 +40981,30 @@ __metadata: languageName: node linkType: hard -"oxc-resolver@npm:^11.15.0": - version: 11.16.3 - resolution: "oxc-resolver@npm:11.16.3" +"oxc-resolver@npm:^11.19.1": + version: 11.19.1 + resolution: "oxc-resolver@npm:11.19.1" dependencies: - "@oxc-resolver/binding-android-arm-eabi": "npm:11.16.3" - "@oxc-resolver/binding-android-arm64": "npm:11.16.3" - "@oxc-resolver/binding-darwin-arm64": "npm:11.16.3" - "@oxc-resolver/binding-darwin-x64": "npm:11.16.3" - "@oxc-resolver/binding-freebsd-x64": "npm:11.16.3" - "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.16.3" - "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.16.3" - "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.16.3" - "@oxc-resolver/binding-linux-arm64-musl": "npm:11.16.3" - "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.16.3" - "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.16.3" - "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.16.3" - "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.16.3" - "@oxc-resolver/binding-linux-x64-gnu": "npm:11.16.3" - "@oxc-resolver/binding-linux-x64-musl": "npm:11.16.3" - "@oxc-resolver/binding-openharmony-arm64": "npm:11.16.3" - "@oxc-resolver/binding-wasm32-wasi": "npm:11.16.3" - "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.16.3" - "@oxc-resolver/binding-win32-ia32-msvc": "npm:11.16.3" - "@oxc-resolver/binding-win32-x64-msvc": "npm:11.16.3" + "@oxc-resolver/binding-android-arm-eabi": "npm:11.19.1" + "@oxc-resolver/binding-android-arm64": "npm:11.19.1" + "@oxc-resolver/binding-darwin-arm64": "npm:11.19.1" + "@oxc-resolver/binding-darwin-x64": "npm:11.19.1" + "@oxc-resolver/binding-freebsd-x64": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm-gnueabihf": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm-musleabihf": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-arm64-musl": "npm:11.19.1" + "@oxc-resolver/binding-linux-ppc64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-riscv64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-riscv64-musl": "npm:11.19.1" + "@oxc-resolver/binding-linux-s390x-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-x64-gnu": "npm:11.19.1" + "@oxc-resolver/binding-linux-x64-musl": "npm:11.19.1" + "@oxc-resolver/binding-openharmony-arm64": "npm:11.19.1" + "@oxc-resolver/binding-wasm32-wasi": "npm:11.19.1" + "@oxc-resolver/binding-win32-arm64-msvc": "npm:11.19.1" + "@oxc-resolver/binding-win32-ia32-msvc": "npm:11.19.1" + "@oxc-resolver/binding-win32-x64-msvc": "npm:11.19.1" dependenciesMeta: "@oxc-resolver/binding-android-arm-eabi": optional: true @@ -41464,7 +41046,7 @@ __metadata: optional: true "@oxc-resolver/binding-win32-x64-msvc": optional: true - checksum: 10/2d0da140e34a74347d03a233488ad0284bbb252a6d250b7ca78d5a431d04d72b8275d896b8fb0d03846f221bcbcb2395739187d2645bf84d1d1332b74b7720be + checksum: 10/a6c8fdb2ef4bf9bb84f28e58685457de427d31f74373c0fbd6d1106010cab33027fa3b4336b1b86d0df0a089cd73a6060b730b1b24974d56c59f6fa29c559f9d languageName: node linkType: hard @@ -41710,10 +41292,10 @@ __metadata: languageName: node linkType: hard -"packageurl-js@npm:1.2.0": - version: 1.2.0 - resolution: "packageurl-js@npm:1.2.0" - checksum: 10/b780ad6cf9f75055effafe8fbed37617eb1924e3dc5b055fb3ecceaaaa93da73ea1508a3874b04bd13342a77bd852b70a4e52596c171cbc57840c4b8452d2d56 +"packageurl-js@npm:2.0.1": + version: 2.0.1 + resolution: "packageurl-js@npm:2.0.1" + checksum: 10/5fdb2b89e5c39dbb87806ef303bc558da0f8c178b8afb2647979d49212039f2cccc6c0135816819d061c6b12b47e8c6bb8c34a2b9fdd8684b9fb975dcf3bc73b languageName: node linkType: hard @@ -42125,13 +41707,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 - languageName: node - linkType: hard - "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -42149,13 +41724,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0": - version: 2.0.0 - resolution: "path-scurry@npm:2.0.0" +"path-scurry@npm:^2.0.0, path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" dependencies: lru-cache: "npm:^11.0.0" minipass: "npm:^7.1.2" - checksum: 10/285ae0c2d6c34ae91dc1d5378ede21981c9a2f6de1ea9ca5a88b5a270ce9763b83dbadc7a324d512211d8d36b0c540427d3d0817030849d97a60fa840a2c59ec + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 languageName: node linkType: hard @@ -42194,13 +41769,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" @@ -42287,10 +41855,10 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.11.0, pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0": - version: 2.11.0 - resolution: "pg-connection-string@npm:2.11.0" - checksum: 10/0333bb1b7ddeac6fa5262920f82a983222c600d21ef14fdc5254b0d3cbb1763030d20c1e8e3c20fa767a6eda8f4b4773550954c06f3e072e5288b6fa9e9cae13 +"pg-connection-string@npm:^2.12.0, pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0": + version: 2.12.0 + resolution: "pg-connection-string@npm:2.12.0" + checksum: 10/03e5462c1f4da57166344a9eae105f95af6887a501dfe4ca2feb85ca8d351162afe9fc581cb27d44ecdaecc59a8bd55a1ec35b66714b01a9db148f0a91c0b36c languageName: node linkType: hard @@ -42308,19 +41876,19 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.11.0": - version: 3.11.0 - resolution: "pg-pool@npm:3.11.0" +"pg-pool@npm:^3.13.0": + version: 3.13.0 + resolution: "pg-pool@npm:3.13.0" peerDependencies: pg: ">=8.0" - checksum: 10/51c77d99f17cf791333467352df8326e0f70f9c517eada65a5e7819b2422f6e655e52319f5406eb578504442ae5f399b6e1d023e41d0c199aaf82879a890db6d + checksum: 10/0addd11b3f3f49fb1d16bf181583411e8a9809730dbfc5edb9bbf5110bc02beb4ac346c52a121f446ff76bcd420a76a3409952ea743aa5f6a04705ea953bd8aa languageName: node linkType: hard -"pg-protocol@npm:*, pg-protocol@npm:^1.11.0": - version: 1.11.0 - resolution: "pg-protocol@npm:1.11.0" - checksum: 10/a70b1b4a3fc5b1be80dfdd65c829a149b8bd9df7488f9c47e0b51c9413aec5eb6da0a9ae9812891d74cd9f2ee90c0e391984a41b64603e7375fcbb9e07070b08 +"pg-protocol@npm:*, pg-protocol@npm:^1.13.0": + version: 1.13.0 + resolution: "pg-protocol@npm:1.13.0" + checksum: 10/302cd3920df00f178519693557c34949d64c8b3af7e2c12772b14f61547b947e4c761b4ca2319dbba5b0906207bb1b535cbfc7006d40d47fd823e277c2690a71 languageName: node linkType: hard @@ -42338,13 +41906,13 @@ __metadata: linkType: hard "pg@npm:^8.11.3, pg@npm:^8.9.0": - version: 8.18.0 - resolution: "pg@npm:8.18.0" + version: 8.20.0 + resolution: "pg@npm:8.20.0" dependencies: pg-cloudflare: "npm:^1.3.0" - pg-connection-string: "npm:^2.11.0" - pg-pool: "npm:^3.11.0" - pg-protocol: "npm:^1.11.0" + pg-connection-string: "npm:^2.12.0" + pg-pool: "npm:^3.13.0" + pg-protocol: "npm:^1.13.0" pg-types: "npm:2.2.0" pgpass: "npm:1.0.5" peerDependencies: @@ -42355,7 +41923,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10/91c622f179f60df08ab7aa9b05a890567ea47f2d7984377b64e88e1eba1c42787324b7fc5ff00e109a757f3329dc4b57c73502603ae2765d1827b2082abbdcfa + checksum: 10/a30b99c799eddbd4f7f98bef906fe25e17c17d7d8918bc7545108947388df0d9c490858b04dabd37ce0063f40b1e58987b0079f99d5a661fb66f40c17f172bfc languageName: node linkType: hard @@ -42424,15 +41992,6 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:^0.6.0": - version: 0.6.0 - resolution: "pidtree@npm:0.6.0" - bin: - pidtree: bin/pidtree.js - checksum: 10/ea67fb3159e170fd069020e0108ba7712df9f0fd13c8db9b2286762856ddce414fb33932e08df4bfe36e91fe860b51852aee49a6f56eb4714b69634343add5df - languageName: node - linkType: hard - "pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -42612,13 +42171,6 @@ __metadata: languageName: node linkType: hard -"pluralize@npm:^7.0.0": - version: 7.0.0 - resolution: "pluralize@npm:7.0.0" - checksum: 10/905274e679d3802650fdfdd977434757d4680082da7a23c0938a608d1d5c8246790b62dc15ff1f737b0d57baa6ad2f6ebb0857b1950435a583e32af76ee58e1f - languageName: node - linkType: hard - "pony-cause@npm:^1.1.1": version: 1.1.1 resolution: "pony-cause@npm:1.1.1" @@ -43101,13 +42653,13 @@ __metadata: linkType: hard "postcss@npm:^8.1.0, postcss@npm:^8.4.33, postcss@npm:^8.5.1, postcss@npm:^8.5.6": - version: 8.5.6 - resolution: "postcss@npm:8.5.6" + version: 8.5.8 + resolution: "postcss@npm:8.5.8" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86 + checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 languageName: node linkType: hard @@ -43169,7 +42721,7 @@ __metadata: languageName: node linkType: hard -"prebuild-install@npm:^7.1.1, prebuild-install@npm:^7.1.3": +"prebuild-install@npm:^7.0.1, prebuild-install@npm:^7.1.1, prebuild-install@npm:^7.1.3": version: 7.1.3 resolution: "prebuild-install@npm:7.1.3" dependencies: @@ -43235,13 +42787,6 @@ __metadata: languageName: node linkType: hard -"prelude-ls@npm:~1.1.2": - version: 1.1.2 - resolution: "prelude-ls@npm:1.1.2" - checksum: 10/946a9f60d3477ca6b7d4c5e8e452ad1b98dc8aaa992cea939a6b926ac16cc4129d7217c79271dc808b5814b1537ad0af37f29a942e2eafbb92cfc5a1c87c38cb - languageName: node - linkType: hard - "prepend-http@npm:^2.0.0": version: 2.0.0 resolution: "prepend-http@npm:2.0.0" @@ -43249,13 +42794,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" @@ -43265,7 +42803,7 @@ __metadata: languageName: node linkType: hard -"pretty-bytes@npm:^5.1.0, pretty-bytes@npm:^5.3.0": +"pretty-bytes@npm:^5.3.0": version: 5.6.0 resolution: "pretty-bytes@npm:5.6.0" checksum: 10/9c082500d1e93434b5b291bd651662936b8bd6204ec9fa17d563116a192d6d86b98f6d328526b4e8d783c07d5499e2614a807520249692da9ec81564b2f439cd @@ -43406,15 +42944,6 @@ __metadata: languageName: node linkType: hard -"promise-deferred@npm:^2.0.3": - version: 2.0.4 - resolution: "promise-deferred@npm:2.0.4" - dependencies: - promise: "npm:^8.3.0" - checksum: 10/1d0e306d54a7436e288836c0784abdf11798011a6c3309f4ce8e24564ba958c41ca0d21bb7ec95386f04ac8f9691fdd8e3dd0af5176b496a2303d00db96acf5a - languageName: node - linkType: hard - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -43462,25 +42991,6 @@ __metadata: languageName: node linkType: hard -"promise@npm:^8.3.0": - version: 8.3.0 - resolution: "promise@npm:8.3.0" - dependencies: - asap: "npm:~2.0.6" - checksum: 10/55e9d0d723c66810966bc055c6c77a3658c0af7e4a8cc88ea47aeaf2949ca0bd1de327d9c631df61236f5406ad478384fa19a77afb3f88c0303eba9e5eb0a8d8 - languageName: node - linkType: hard - -"promiseback@npm:^2.0.2": - version: 2.0.3 - resolution: "promiseback@npm:2.0.3" - dependencies: - is-callable: "npm:^1.1.5" - promise-deferred: "npm:^2.0.3" - checksum: 10/39716e64ac75b3a5c58532493f594d4788267ee13e2aeee5c60b448eb17e8f98c8ff4778c5497aed1594e29c428710ae21c83671c87c24b3d2c42f0c359d6e55 - languageName: node - linkType: hard - "prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -43513,12 +43023,13 @@ __metadata: languageName: node linkType: hard -"properties-reader@npm:^2.3.0": - version: 2.3.0 - resolution: "properties-reader@npm:2.3.0" +"properties-reader@npm:^3.0.1": + version: 3.0.1 + resolution: "properties-reader@npm:3.0.1" dependencies: - mkdirp: "npm:^1.0.4" - checksum: 10/0b41eb4136dc278ae0d97968ccce8de2d48d321655b319192e31f2424f1c6e052182204671e65aa8967216360cb3e7cbd9129830062e058fe9d6a1d74964c29a + "@kwsites/file-exists": "npm:^1.1.1" + mkdirp: "npm:^3.0.1" + checksum: 10/5a96c33dad9925c399bf3c3e343f3eb801fa1b4802330d1bfbf1e7d7639dafa935deb6c60be38b292544b57793b03ba1eaf5191cc3a7d018cda7bd6ead19a15c languageName: node linkType: hard @@ -43574,9 +43085,9 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.0, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0": - version: 7.5.3 - resolution: "protobufjs@npm:7.5.3" +"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0, protobufjs@npm:^7.5.3": + version: 7.5.4 + resolution: "protobufjs@npm:7.5.4" dependencies: "@protobufjs/aspromise": "npm:^1.1.2" "@protobufjs/base64": "npm:^1.1.2" @@ -43590,7 +43101,7 @@ __metadata: "@protobufjs/utf8": "npm:^1.1.0" "@types/node": "npm:>=13.7.0" long: "npm:^5.0.0" - checksum: 10/3e412d2e2f799875dcdac1b43508f465a499dd3ffd9d24073669702589ef016529905617a12a967b1a8cfbe803a638b494cc3ed8db035605c7570d1a7fc094c9 + checksum: 10/88d677bb6f11a2ecec63fdd053dfe6d31120844d04e865efa9c8fbe0674cd077d6624ecfdf014018a20dcb114ae2a59c1b21966dd8073e920650c71370966439 languageName: node linkType: hard @@ -43672,7 +43183,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^1.3.2, punycode@npm:^1.4.1": +"punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" checksum: 10/af2700dde1a116791ff8301348ff344c47d6c224e875057237d1b5112035655fb07a6175cfdb8bf0e3a8cdfd2dc82b3a622e0aefd605566c0e949a6d0d1256a4 @@ -43725,7 +43236,16 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.14.1, qs@npm:^6.7.0, qs@npm:^6.9.4, qs@npm:~6.14.0": +"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.14.1, qs@npm:^6.7.0, qs@npm:^6.9.4": + version: 6.15.0 + resolution: "qs@npm:6.15.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/a3458f2f389285c3512e0ebc55522ee370ac7cb720ba9f0eff3e30fb2bb07631caf556c08e2a3d4481a371ac14faa9ceb7442a0610c5a7e55b23a5bdee7b701c + languageName: node + linkType: hard + +"qs@npm:~6.14.0": version: 6.14.2 resolution: "qs@npm:6.14.2" dependencies: @@ -43902,6 +43422,13 @@ __metadata: languageName: node linkType: hard +"rate-limiter-flexible@npm:^8.0.1": + version: 8.3.0 + resolution: "rate-limiter-flexible@npm:8.3.0" + checksum: 10/9c8d7a3224e3a57fdf7da721dabdb4942eb7dd7b5f3aa4cd57e2185928d5842855a8dc2e1db5e0403987d369972d0567e98aade2c2daa71436ebdae704e5a8ff + languageName: node + linkType: hard + "raw-body@npm:^2.3.3, raw-body@npm:^2.4.1, raw-body@npm:~2.5.3": version: 2.5.3 resolution: "raw-body@npm:2.5.3" @@ -43988,95 +43515,95 @@ __metadata: linkType: hard "react-aria-components@npm:^1.14.0": - version: 1.14.0 - resolution: "react-aria-components@npm:1.14.0" + version: 1.16.0 + resolution: "react-aria-components@npm:1.16.0" dependencies: - "@internationalized/date": "npm:^3.10.1" + "@internationalized/date": "npm:^3.12.0" "@internationalized/string": "npm:^3.2.7" - "@react-aria/autocomplete": "npm:3.0.0-rc.4" - "@react-aria/collections": "npm:^3.0.1" - "@react-aria/dnd": "npm:^3.11.4" - "@react-aria/focus": "npm:^3.21.3" - "@react-aria/interactions": "npm:^3.26.0" + "@react-aria/autocomplete": "npm:3.0.0-rc.6" + "@react-aria/collections": "npm:^3.0.3" + "@react-aria/dnd": "npm:^3.11.6" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/interactions": "npm:^3.27.1" "@react-aria/live-announcer": "npm:^3.4.4" - "@react-aria/overlays": "npm:^3.31.0" + "@react-aria/overlays": "npm:^3.31.2" "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/textfield": "npm:^3.18.3" - "@react-aria/toolbar": "npm:3.0.0-beta.22" - "@react-aria/utils": "npm:^3.32.0" - "@react-aria/virtualizer": "npm:^4.1.11" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/toolbar": "npm:3.0.0-beta.24" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/virtualizer": "npm:^4.1.13" "@react-stately/autocomplete": "npm:3.0.0-beta.4" - "@react-stately/layout": "npm:^4.5.2" - "@react-stately/selection": "npm:^3.20.7" - "@react-stately/table": "npm:^3.15.2" + "@react-stately/layout": "npm:^4.6.0" + "@react-stately/selection": "npm:^3.20.9" + "@react-stately/table": "npm:^3.15.4" "@react-stately/utils": "npm:^3.11.0" - "@react-stately/virtualizer": "npm:^4.4.4" - "@react-types/form": "npm:^3.7.16" - "@react-types/grid": "npm:^3.3.6" - "@react-types/shared": "npm:^3.32.1" - "@react-types/table": "npm:^3.13.4" + "@react-stately/virtualizer": "npm:^4.4.6" + "@react-types/form": "npm:^3.7.18" + "@react-types/grid": "npm:^3.3.8" + "@react-types/shared": "npm:^3.33.1" + "@react-types/table": "npm:^3.13.6" "@swc/helpers": "npm:^0.5.0" client-only: "npm:^0.0.1" - react-aria: "npm:^3.45.0" - react-stately: "npm:^3.43.0" - use-sync-external-store: "npm:^1.4.0" + react-aria: "npm:^3.47.0" + react-stately: "npm:^3.45.0" + use-sync-external-store: "npm:^1.6.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/cc842f03a6cb10a11ac6c95fbdcc97329a2bce7cf9203bd3fc9f4f4ea248bd308bc31b39f6df3c6b427be5adc370813e04b7a4cf41e8a0193195dde048462d9b + checksum: 10/091ff728a31b703e2a0c9218fab9486c797773054676380080b050c5af4afb614c9822c3cf08b7f932acec8eed4cd0b04fad7ac78cb782827e8352851dc50220 languageName: node linkType: hard -"react-aria@npm:^3.45.0": - version: 3.46.0 - resolution: "react-aria@npm:3.46.0" +"react-aria@npm:^3.47.0": + version: 3.47.0 + resolution: "react-aria@npm:3.47.0" dependencies: "@internationalized/string": "npm:^3.2.7" - "@react-aria/breadcrumbs": "npm:^3.5.31" - "@react-aria/button": "npm:^3.14.4" - "@react-aria/calendar": "npm:^3.9.4" - "@react-aria/checkbox": "npm:^3.16.4" - "@react-aria/color": "npm:^3.1.4" - "@react-aria/combobox": "npm:^3.14.2" - "@react-aria/datepicker": "npm:^3.16.0" - "@react-aria/dialog": "npm:^3.5.33" - "@react-aria/disclosure": "npm:^3.1.2" - "@react-aria/dnd": "npm:^3.11.5" - "@react-aria/focus": "npm:^3.21.4" - "@react-aria/gridlist": "npm:^3.14.3" - "@react-aria/i18n": "npm:^3.12.15" - "@react-aria/interactions": "npm:^3.27.0" - "@react-aria/label": "npm:^3.7.24" - "@react-aria/landmark": "npm:^3.0.9" - "@react-aria/link": "npm:^3.8.8" - "@react-aria/listbox": "npm:^3.15.2" - "@react-aria/menu": "npm:^3.20.0" - "@react-aria/meter": "npm:^3.4.29" - "@react-aria/numberfield": "npm:^3.12.4" - "@react-aria/overlays": "npm:^3.31.1" - "@react-aria/progress": "npm:^3.4.29" - "@react-aria/radio": "npm:^3.12.4" - "@react-aria/searchfield": "npm:^3.8.11" - "@react-aria/select": "npm:^3.17.2" - "@react-aria/selection": "npm:^3.27.1" - "@react-aria/separator": "npm:^3.4.15" - "@react-aria/slider": "npm:^3.8.4" + "@react-aria/breadcrumbs": "npm:^3.5.32" + "@react-aria/button": "npm:^3.14.5" + "@react-aria/calendar": "npm:^3.9.5" + "@react-aria/checkbox": "npm:^3.16.5" + "@react-aria/color": "npm:^3.1.5" + "@react-aria/combobox": "npm:^3.15.0" + "@react-aria/datepicker": "npm:^3.16.1" + "@react-aria/dialog": "npm:^3.5.34" + "@react-aria/disclosure": "npm:^3.1.3" + "@react-aria/dnd": "npm:^3.11.6" + "@react-aria/focus": "npm:^3.21.5" + "@react-aria/gridlist": "npm:^3.14.4" + "@react-aria/i18n": "npm:^3.12.16" + "@react-aria/interactions": "npm:^3.27.1" + "@react-aria/label": "npm:^3.7.25" + "@react-aria/landmark": "npm:^3.0.10" + "@react-aria/link": "npm:^3.8.9" + "@react-aria/listbox": "npm:^3.15.3" + "@react-aria/menu": "npm:^3.21.0" + "@react-aria/meter": "npm:^3.4.30" + "@react-aria/numberfield": "npm:^3.12.5" + "@react-aria/overlays": "npm:^3.31.2" + "@react-aria/progress": "npm:^3.4.30" + "@react-aria/radio": "npm:^3.12.5" + "@react-aria/searchfield": "npm:^3.8.12" + "@react-aria/select": "npm:^3.17.3" + "@react-aria/selection": "npm:^3.27.2" + "@react-aria/separator": "npm:^3.4.16" + "@react-aria/slider": "npm:^3.8.5" "@react-aria/ssr": "npm:^3.9.10" - "@react-aria/switch": "npm:^3.7.10" - "@react-aria/table": "npm:^3.17.10" - "@react-aria/tabs": "npm:^3.11.0" - "@react-aria/tag": "npm:^3.8.0" - "@react-aria/textfield": "npm:^3.18.4" - "@react-aria/toast": "npm:^3.0.10" - "@react-aria/tooltip": "npm:^3.9.1" - "@react-aria/tree": "npm:^3.1.6" - "@react-aria/utils": "npm:^3.33.0" - "@react-aria/visually-hidden": "npm:^3.8.30" - "@react-types/shared": "npm:^3.33.0" + "@react-aria/switch": "npm:^3.7.11" + "@react-aria/table": "npm:^3.17.11" + "@react-aria/tabs": "npm:^3.11.1" + "@react-aria/tag": "npm:^3.8.1" + "@react-aria/textfield": "npm:^3.18.5" + "@react-aria/toast": "npm:^3.0.11" + "@react-aria/tooltip": "npm:^3.9.2" + "@react-aria/tree": "npm:^3.1.7" + "@react-aria/utils": "npm:^3.33.1" + "@react-aria/visually-hidden": "npm:^3.8.31" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/6fe3456515a60bb86d66180920ec56ad120184e4057c7e0a2165e2e54c8709c20ba90fcec51b2c5fdfbad586042ba445a8c0fab780953b0d808ac00b6dd4f28b + checksum: 10/9afb9079939553d222c09630fd8e59949c586a716500a79298fff0f5c830a6f1c83dc017a74811c42f5606ddae4d1f4f1e724c803d70c073cd5598b5eb07140e languageName: node linkType: hard @@ -44110,6 +43637,15 @@ __metadata: languageName: node linkType: hard +"react-compiler-runtime@npm:19.1.0-rc.1": + version: 19.1.0-rc.1 + resolution: "react-compiler-runtime@npm:19.1.0-rc.1" + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + checksum: 10/f64a63f8a6eacaebf87b2065dea9b2adc90d061c94361b31233284cbacc3fcf5f7dce6b5353ac8862ca5aa0a0d92240e3d6d695afa73d6bc5ce6d347b4c66870 + languageName: node + linkType: hard + "react-copy-to-clipboard@npm:5.1.0, react-copy-to-clipboard@npm:^5.0.4": version: 5.1.0 resolution: "react-copy-to-clipboard@npm:5.1.0" @@ -44340,11 +43876,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.12.2": - version: 7.71.1 - resolution: "react-hook-form@npm:7.71.1" + version: 7.71.2 + resolution: "react-hook-form@npm:7.71.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/54b56aa52a6ac17fb5aa68ce072b08f50b75c2705bf878069f817c346e464c1ebfbf67be372d94cf8e620fa07ca8e64ffca71bb69071dd0290a1ef8f43eefb9d + checksum: 10/4cc90868016f8463463ea5d1812f9405832e02fdb12f18ff0262c6437e7a9cdfe6443a7f58decf903cb8f20bfd68c0ed419283b0d6be886f3e84dc4d14b0efa6 languageName: node linkType: hard @@ -44518,10 +44054,10 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:^0.17.0": - version: 0.17.0 - resolution: "react-refresh@npm:0.17.0" - checksum: 10/5e94f07d43bb1cfdc9b0c6e0c8c73e754005489950dcff1edb53aa8451d1d69a47b740b195c7c80fb4eb511c56a3585dc55eddd83f0097fb5e015116a1460467 +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10/504c331c19776bf8320c23bad7f80b3a28de03301ed7523b0dd21d3f02bf2b53bbdd5aa52469b187bc90f358614b2ba303c088a0765c95f4f0a68c43a7d67b1d languageName: node linkType: hard @@ -44534,38 +44070,38 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.3": - version: 2.3.4 - resolution: "react-remove-scroll-bar@npm:2.3.4" +"react-remove-scroll-bar@npm:^2.3.7": + version: 2.3.8 + resolution: "react-remove-scroll-bar@npm:2.3.8" dependencies: - react-style-singleton: "npm:^2.2.1" + react-style-singleton: "npm:^2.2.2" tslib: "npm:^2.0.0" peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10/ac028b3ed12e66972cab8656747736729b219dff5a600178d1650300a2a750ace37f7ec82146147d37b092b19874f45cf7a45edceff68ac1f59607a828ca089f + checksum: 10/6c0f8cff98b9f49a4ee2263f1eedf12926dced5ce220fbe83bd93544460e2a7ec8ec39b35d1b2a75d2fced0b2d64afeb8e66f830431ca896e05a20585f9fc350 languageName: node linkType: hard -"react-remove-scroll@npm:2.5.5": - version: 2.5.5 - resolution: "react-remove-scroll@npm:2.5.5" +"react-remove-scroll@npm:^2.6.3": + version: 2.7.2 + resolution: "react-remove-scroll@npm:2.7.2" dependencies: - react-remove-scroll-bar: "npm:^2.3.3" - react-style-singleton: "npm:^2.2.1" + react-remove-scroll-bar: "npm:^2.3.7" + react-style-singleton: "npm:^2.2.3" tslib: "npm:^2.1.0" - use-callback-ref: "npm:^1.3.0" - use-sidecar: "npm:^1.1.2" + use-callback-ref: "npm:^1.3.3" + use-sidecar: "npm:^1.1.3" peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/f0646ac384ce3852d1f41e30a9f9e251b11cf3b430d1d114c937c8fa7f90a895c06378d0d6b6ff0b2d00cbccf15e845921944fd6074ae67a0fb347a718106d88 + checksum: 10/6a7858f9a3eb57128abb64479ced78283b0cbee0b43c2b87e42711e75c564c27d1d3dd2b81f2ea95c80cb9e6b6965d1c9e2332f6a7e7b753cd8aeb02b9b97704 languageName: node linkType: hard @@ -44687,56 +44223,55 @@ __metadata: languageName: node linkType: hard -"react-stately@npm:^3.43.0": - version: 3.43.0 - resolution: "react-stately@npm:3.43.0" +"react-stately@npm:^3.45.0": + version: 3.45.0 + resolution: "react-stately@npm:3.45.0" dependencies: - "@react-stately/calendar": "npm:^3.9.1" - "@react-stately/checkbox": "npm:^3.7.3" - "@react-stately/collections": "npm:^3.12.8" - "@react-stately/color": "npm:^3.9.3" - "@react-stately/combobox": "npm:^3.12.1" - "@react-stately/data": "npm:^3.15.0" - "@react-stately/datepicker": "npm:^3.15.3" - "@react-stately/disclosure": "npm:^3.0.9" - "@react-stately/dnd": "npm:^3.7.2" - "@react-stately/form": "npm:^3.2.2" - "@react-stately/list": "npm:^3.13.2" - "@react-stately/menu": "npm:^3.9.9" - "@react-stately/numberfield": "npm:^3.10.3" - "@react-stately/overlays": "npm:^3.6.21" - "@react-stately/radio": "npm:^3.11.3" - "@react-stately/searchfield": "npm:^3.5.17" - "@react-stately/select": "npm:^3.9.0" - "@react-stately/selection": "npm:^3.20.7" - "@react-stately/slider": "npm:^3.7.3" - "@react-stately/table": "npm:^3.15.2" - "@react-stately/tabs": "npm:^3.8.7" - "@react-stately/toast": "npm:^3.1.2" - "@react-stately/toggle": "npm:^3.9.3" - "@react-stately/tooltip": "npm:^3.5.9" - "@react-stately/tree": "npm:^3.9.4" - "@react-types/shared": "npm:^3.32.1" + "@react-stately/calendar": "npm:^3.9.3" + "@react-stately/checkbox": "npm:^3.7.5" + "@react-stately/collections": "npm:^3.12.10" + "@react-stately/color": "npm:^3.9.5" + "@react-stately/combobox": "npm:^3.13.0" + "@react-stately/data": "npm:^3.15.2" + "@react-stately/datepicker": "npm:^3.16.1" + "@react-stately/disclosure": "npm:^3.0.11" + "@react-stately/dnd": "npm:^3.7.4" + "@react-stately/form": "npm:^3.2.4" + "@react-stately/list": "npm:^3.13.4" + "@react-stately/menu": "npm:^3.9.11" + "@react-stately/numberfield": "npm:^3.11.0" + "@react-stately/overlays": "npm:^3.6.23" + "@react-stately/radio": "npm:^3.11.5" + "@react-stately/searchfield": "npm:^3.5.19" + "@react-stately/select": "npm:^3.9.2" + "@react-stately/selection": "npm:^3.20.9" + "@react-stately/slider": "npm:^3.7.5" + "@react-stately/table": "npm:^3.15.4" + "@react-stately/tabs": "npm:^3.8.9" + "@react-stately/toast": "npm:^3.1.3" + "@react-stately/toggle": "npm:^3.9.5" + "@react-stately/tooltip": "npm:^3.5.11" + "@react-stately/tree": "npm:^3.9.6" + "@react-types/shared": "npm:^3.33.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/a82d9ba6fdb9a922cb0820de2678be548fa08fc011a9b0be35f3a0022049219810ce1696b7b9104db907b3103233143801649b584d8a06fec582dfb78c81dcf9 + checksum: 10/5a34d8832b3867e06582bba57dbfd5dfbacd2ee7489ab8fa21ff25d6f0716e9fbf018d2390d5c5af62b99d53f3100c703b372234b0421934a3910e3160ec8d4a languageName: node linkType: hard -"react-style-singleton@npm:^2.2.1": - version: 2.2.1 - resolution: "react-style-singleton@npm:2.2.1" +"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": + version: 2.2.3 + resolution: "react-style-singleton@npm:2.2.3" dependencies: get-nonce: "npm:^1.0.0" - invariant: "npm:^2.2.4" tslib: "npm:^2.0.0" peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/80c58fd6aac3594e351e2e7b048d8a5b09508adb21031a38b3c40911fe58295572eddc640d4b20a7be364842c8ed1120fe30097e22ea055316b375b88d4ff02a + checksum: 10/62498094ff3877a37f351b29e6cad9e38b2eb1ac3c0cb27ebf80aee96554f80b35e17bdb552bcd7ac8b7cb9904fea93ea5668f2057c73d38f90b5d46bb9b27ab languageName: node linkType: hard @@ -45783,7 +45318,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: @@ -45928,6 +45463,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:*" @@ -45943,7 +45479,7 @@ __metadata: "@storybook/addon-a11y": "npm:^10.3.0-alpha.1" "@storybook/addon-docs": "npm:^10.3.0-alpha.1" "@storybook/addon-links": "npm:^10.3.0-alpha.1" - "@storybook/addon-mcp": "npm:^0.2.2" + "@storybook/addon-mcp": "npm:^0.3.0" "@storybook/addon-themes": "npm:^10.3.0-alpha.1" "@storybook/addon-vitest": "npm:^10.3.0-alpha.1" "@storybook/react-vite": "npm:^10.3.0-alpha.1" @@ -45970,7 +45506,7 @@ __metadata: jest: "npm:^30" js-yaml: "npm:^4.1.1" jsdom: "npm:^27" - lint-staged: "npm:^15.0.0" + lint-staged: "npm:^16.0.0" madge: "npm:^8.0.0" minimist: "npm:^1.2.5" node-gyp: "npm:^10.0.0" @@ -45978,7 +45514,7 @@ __metadata: semver: "npm:^7.5.3" shx: "npm:^0.4.0" sloc: "npm:^0.3.1" - sort-package-json: "npm:^2.8.0" + sort-package-json: "npm:^3.0.0" storybook: "npm:^10.3.0-alpha.1" ts-morph: "npm:^24.0.0" typedoc: "npm:^0.28.0" @@ -46041,6 +45577,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^3.0.0": + version: 3.0.0 + resolution: "run-async@npm:3.0.0" + checksum: 10/97fb8747f7765b77ebcd311d3a33548099336f04c6434e0763039b98c1de0f1b4421000695aff8751f309c0b995d8dfd620c1f1e4c35572da38c101488165305 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -46162,7 +45705,7 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:~2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 @@ -46181,6 +45724,13 @@ __metadata: languageName: node linkType: hard +"sax@npm:^1.5.0": + version: 1.5.0 + resolution: "sax@npm:1.5.0" + checksum: 10/9012ff37dda7a7ac5da45db2143b04036103e8bef8d586c3023afd5df6caf0ebd7f38017eee344ad2e2247eded7d38e9c42cf291d8dd91781352900ac0fd2d9f + languageName: node + linkType: hard + "saxes@npm:^6.0.0": version: 6.0.0 resolution: "saxes@npm:6.0.0" @@ -46386,11 +45936,11 @@ __metadata: linkType: hard "semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.2, semver@npm:^7.7.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 languageName: node linkType: hard @@ -46455,13 +46005,6 @@ __metadata: languageName: node linkType: hard -"seq-queue@npm:^0.0.5": - version: 0.0.5 - resolution: "seq-queue@npm:0.0.5" - checksum: 10/fa302e3b2aaece644532603ae42d675f9b8750e395a98740dd58dc5e02985ce6f0c2b78715b5984d6f6a807893735a14212a70d6ec591e6fba410397269588a0 - languageName: node - linkType: hard - "serialize-error@npm:^7.0.1": version: 7.0.1 resolution: "serialize-error@npm:7.0.1" @@ -46480,27 +46023,18 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.2": - version: 6.0.2 - resolution: "serialize-javascript@npm:6.0.2" - dependencies: - randombytes: "npm:^2.1.0" - checksum: 10/445a420a6fa2eaee4b70cbd884d538e259ab278200a2ededd73253ada17d5d48e91fb1f4cd224a236ab62ea7ba0a70c6af29fc93b4f3d3078bf7da1c031fde58 - languageName: node - linkType: hard - "serve-handler@npm:^6.1.3": - version: 6.1.6 - resolution: "serve-handler@npm:6.1.6" + version: 6.1.7 + resolution: "serve-handler@npm:6.1.7" dependencies: bytes: "npm:3.0.0" content-disposition: "npm:0.5.2" mime-types: "npm:2.1.18" - minimatch: "npm:3.1.2" + minimatch: "npm:3.1.5" path-is-inside: "npm:1.0.2" path-to-regexp: "npm:3.3.0" range-parser: "npm:1.2.0" - checksum: 10/7e7d93eb7e69fcd9f9c5afc2ef2b46cb0072b4af13cbabef9bca725afb350ddae6857d8c8be2c256f7ce1f7677c20347801399c11caa5805c0090339f894e8f2 + checksum: 10/2366e53cc8e8376d58abb289293b930111fa5da6d14bb31eafac5b1162f332c45c6f394c7d78fdcf6b5736e12caf9370b02d05c7e8a75291d2fc6a55b52b14ea languageName: node linkType: hard @@ -46885,13 +46419,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" @@ -46914,16 +46441,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^5.0.0": - version: 5.0.0 - resolution: "slice-ansi@npm:5.0.0" - dependencies: - ansi-styles: "npm:^6.0.0" - is-fullwidth-code-point: "npm:^4.0.0" - checksum: 10/7e600a2a55e333a21ef5214b987c8358fe28bfb03c2867ff2cbf919d62143d1812ac27b4297a077fdaf27a03da3678e49551c93e35f9498a3d90221908a1180e - languageName: node - linkType: hard - "slice-ansi@npm:^7.1.0": version: 7.1.0 resolution: "slice-ansi@npm:7.1.0" @@ -46934,6 +46451,16 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^8.0.0": + version: 8.0.0 + resolution: "slice-ansi@npm:8.0.0" + dependencies: + ansi-styles: "npm:^6.2.3" + is-fullwidth-code-point: "npm:^5.1.0" + checksum: 10/6a7e146852047e26dd5857b35c767e52906549c580cce0ad2287cc32f54f5a582494f674817fc9ac21b2e4ac1ddeaa85b3dee409782681b465330278890c73a8 + languageName: node + linkType: hard + "sloc@npm:^0.3.1": version: 0.3.2 resolution: "sloc@npm:0.3.2" @@ -46983,14 +46510,14 @@ __metadata: languageName: node linkType: hard -"snyk-nodejs-lockfile-parser@npm:^1.58.14": - version: 1.60.1 - resolution: "snyk-nodejs-lockfile-parser@npm:1.60.1" +"snyk-nodejs-lockfile-parser@npm:^2.0.0": + version: 2.6.1 + resolution: "snyk-nodejs-lockfile-parser@npm:2.6.1" dependencies: - "@snyk/dep-graph": "npm:^2.3.0" + "@snyk/dep-graph": "npm:^2.12.0" "@snyk/error-catalog-nodejs-public": "npm:^5.16.0" "@snyk/graphlib": "npm:2.1.9-patch.3" - "@yarnpkg/core": "npm:^2.4.0" + "@yarnpkg/core": "npm:^4.4.1" "@yarnpkg/lockfile": "npm:^1.1.0" dependency-path: "npm:^9.2.8" event-loop-spinner: "npm:^2.0.0" @@ -47007,7 +46534,7 @@ __metadata: uuid: "npm:^8.3.0" bin: parse-nodejs-lockfile: bin/index.js - checksum: 10/2186abf1a7930ff12c5a3929c514300a0ecb47f576f64b84b265292c106aba9ec13a98cdae0b2f01449e53311d361a67cbb13ed631e718d28faaafa97971adc5 + checksum: 10/6573f7379f0005f632678b8518fd5559f91548fc3fb5f2004a92defb44e99acc46097382c7017f6f901df89733e521486229b5fcf9e6d1715b508ca1f9d46fff languageName: node linkType: hard @@ -47075,28 +46602,27 @@ __metadata: languageName: node linkType: hard -"sort-object-keys@npm:^1.1.3": - version: 1.1.3 - resolution: "sort-object-keys@npm:1.1.3" - checksum: 10/abea944d6722a1710a1aa6e4f9509da085d93d5fc0db23947cb411eedc7731f80022ce8fa68ed83a53dd2ac7441fcf72a3f38c09b3d9bbc4ff80546aa2e151ad +"sort-object-keys@npm:^2.0.1": + version: 2.1.0 + resolution: "sort-object-keys@npm:2.1.0" + checksum: 10/a0a18a6f4ab601adb889204a83e2664a4810042dd9680e588cabb47fffd40473939ce3fda5a28fdb58bac0762a7b45ad442815465b978d4b243bab840df3d7cd languageName: node linkType: hard -"sort-package-json@npm:^2.8.0": - version: 2.15.1 - resolution: "sort-package-json@npm:2.15.1" +"sort-package-json@npm:^3.0.0": + version: 3.6.1 + resolution: "sort-package-json@npm:3.6.1" dependencies: - detect-indent: "npm:^7.0.1" - detect-newline: "npm:^4.0.0" - get-stdin: "npm:^9.0.0" - git-hooks-list: "npm:^3.0.0" + detect-indent: "npm:^7.0.2" + detect-newline: "npm:^4.0.1" + git-hooks-list: "npm:^4.1.1" is-plain-obj: "npm:^4.1.0" - semver: "npm:^7.6.0" - sort-object-keys: "npm:^1.1.3" - tinyglobby: "npm:^0.2.9" + semver: "npm:^7.7.3" + sort-object-keys: "npm:^2.0.1" + tinyglobby: "npm:^0.2.15" bin: sort-package-json: cli.js - checksum: 10/3378565a07368e00eeb625e6b85d1edf9a3bf9f88ced32423bd3036ddb8c674fb8c0fb559044ef939e6de20bb7550423e992f4abd19cff2398d006f0fe8afc82 + checksum: 10/e4bcf8432b570143e94b6b302fc941fd0b564e8f9f423bc55ae469c81e6f84c35e456f024d8d186a2ec6ca120407444e6ceb5a5f0feb7c877c159ef716f651ed languageName: node linkType: hard @@ -47280,10 +46806,10 @@ __metadata: languageName: node linkType: hard -"sqlstring@npm:^2.3.3": - version: 2.3.3 - resolution: "sqlstring@npm:2.3.3" - checksum: 10/4e5a25af2d77a031fe00694034bf9fd822ddc3a483c9383124b120aa6b9ae9ab71e173cd29fba9c653998ebfef9e97be668957839960b9b3dc1afcb45f1ddb64 +"sql-escaper@npm:^1.3.3": + version: 1.3.3 + resolution: "sql-escaper@npm:1.3.3" + checksum: 10/bc53ff84eba322ba76bd10f95c2a180b477e13453236583dbed3bed48d6ade3ca73922eaa860372c2b3a9427e3345fd1a19d1e3b8d0e36fa1d79c450f113b962 languageName: node linkType: hard @@ -47417,15 +46943,6 @@ __metadata: languageName: node linkType: hard -"static-eval@npm:2.0.2": - version: 2.0.2 - resolution: "static-eval@npm:2.0.2" - dependencies: - escodegen: "npm:^1.8.1" - checksum: 10/2e2faf1b23bad5d9d5b2407b18945c7b97f8706b6d65f06bb3583a2d4fd1994cf5890c5779a1bfa2a02905dc860e077e4f045d7413d289d8993f605758f8992f - languageName: node - linkType: hard - "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -47465,12 +46982,12 @@ __metadata: linkType: hard "storybook@npm:^10.3.0-alpha.1": - version: 10.3.0-alpha.1 - resolution: "storybook@npm:10.3.0-alpha.1" + version: 10.3.0-alpha.14 + resolution: "storybook@npm:10.3.0-alpha.14" dependencies: "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^2.0.1" - "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" "@vitest/spy": "npm:3.2.4" @@ -47487,7 +47004,7 @@ __metadata: optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10/9b76b96109cd9b2b9ff9e50d05dfc211ccf076c1bf1293e47a3f3b65fdc543a37b2ae4d3088edb9e10af5b7a320e9cadde7dc8945fe0ad5f9cd61b779b3e19b0 + checksum: 10/f19d870da20440976de997117839178aa6d169b441dbf27d600f0cdfb1f1bb3730fe0f0a4f665fae59662664b53dd6fef0bed5e6e35c10de89c903b45e0ac43f languageName: node linkType: hard @@ -47552,7 +47069,7 @@ __metadata: languageName: node linkType: hard -"stream-to-array@npm:^2.3.0, stream-to-array@npm:~2.3.0": +"stream-to-array@npm:^2.3.0": version: 2.3.0 resolution: "stream-to-array@npm:2.3.0" dependencies: @@ -47561,17 +47078,6 @@ __metadata: languageName: node linkType: hard -"stream-to-promise@npm:^2.2.0": - version: 2.2.0 - resolution: "stream-to-promise@npm:2.2.0" - dependencies: - any-promise: "npm:~1.3.0" - end-of-stream: "npm:~1.1.0" - stream-to-array: "npm:~2.3.0" - checksum: 10/e4d3253c68dae65c51c5aa1bd657a072267869fd61b57068e74cee7a8e45d67fe154b56918cf546b38cb5be1fa042e632b7267abc9676bb75bba55952d2d57d1 - languageName: node - linkType: hard - "streamroller@npm:^3.1.5": version: 3.1.5 resolution: "streamroller@npm:3.1.5" @@ -47695,6 +47201,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.2.0": + version: 8.2.0 + resolution: "string-width@npm:8.2.0" + dependencies: + get-east-asian-width: "npm:^1.5.0" + strip-ansi: "npm:^7.1.2" + checksum: 10/c4f62877ec08fca155e84a260eb4f58f473cfe5169bd1c1e21ffb563d8e0b7f6d705cc3d250f2ed6bb4f30ee9732ad026f54afaac77aa487e3d1dc1b1969e51b + languageName: node + linkType: hard + "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -47822,12 +47338,12 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": - version: 7.1.2 - resolution: "strip-ansi@npm:7.1.2" +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": + version: 7.2.0 + resolution: "strip-ansi@npm:7.2.0" dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b + ansi-regex: "npm:^6.2.2" + checksum: 10/96da3bc6d73cfba1218625a3d66cf7d37a69bf0920d8735b28f9eeaafcdb6c1fe8440e1ae9eb1ba0ca355dbe8702da872e105e2e939fa93e7851b3cb5dd7d316 languageName: node linkType: hard @@ -47887,13 +47403,6 @@ __metadata: languageName: node linkType: hard -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 10/23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 - languageName: node - linkType: hard - "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -47940,7 +47449,7 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^2.1.0, strnum@npm:^2.1.2": +"strnum@npm:^2.1.2": version: 2.1.2 resolution: "strnum@npm:2.1.2" checksum: 10/7d894dff385e3a5c5b29c012cf0a7ea7962a92c6a299383c3d6db945ad2b6f3e770511356a9774dbd54444c56af1dc7c435dad6466c47293c48173274dd6c631 @@ -48164,33 +47673,34 @@ __metadata: linkType: hard "svgo@npm:^2.7.0": - version: 2.8.0 - resolution: "svgo@npm:2.8.0" + version: 2.8.2 + resolution: "svgo@npm:2.8.2" dependencies: - "@trysound/sax": "npm:0.2.0" commander: "npm:^7.2.0" css-select: "npm:^4.1.3" css-tree: "npm:^1.1.3" csso: "npm:^4.2.0" picocolors: "npm:^1.0.0" + sax: "npm:^1.5.0" stable: "npm:^0.1.8" bin: - svgo: bin/svgo - checksum: 10/2b74544da1a9521852fe2784252d6083b336e32528d0e424ee54d1613f17312edc7020c29fa399086560e96cba42ede4a2205328a08edeefa26de84cd769a64a + svgo: ./bin/svgo + checksum: 10/a0922a2cbbbc51c0162ea7a7d6c5b660fb4fb65e0f05e226ba571cfe8b651fd870072aed2722ef96715fb77829562b351eb72f2b7bf09038ffc88969acaffd0f languageName: node linkType: hard -"swagger-client@npm:^3.36.0": - version: 3.36.0 - resolution: "swagger-client@npm:3.36.0" +"swagger-client@npm:^3.37.0": + version: 3.37.0 + resolution: "swagger-client@npm:3.37.0" dependencies: "@babel/runtime-corejs3": "npm:^7.22.15" "@scarf/scarf": "npm:=1.4.0" - "@swagger-api/apidom-core": "npm:^1.0.0-rc.1" - "@swagger-api/apidom-error": "npm:^1.0.0-rc.1" - "@swagger-api/apidom-json-pointer": "npm:^1.0.0-rc.1" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-rc.1" - "@swagger-api/apidom-reference": "npm:^1.0.0-rc.1" + "@swagger-api/apidom-core": "npm:^1.6.0" + "@swagger-api/apidom-error": "npm:^1.6.0" + "@swagger-api/apidom-json-pointer": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.6.0" + "@swagger-api/apidom-ns-openapi-3-2": "npm:^1.6.0" + "@swagger-api/apidom-reference": "npm:^1.6.0" "@swaggerexpert/cookie": "npm:^2.0.2" deepmerge: "npm:~4.3.0" fast-json-patch: "npm:^3.0.0-1" @@ -48202,13 +47712,13 @@ __metadata: openapi-server-url-templating: "npm:^1.3.0" ramda: "npm:^0.30.1" ramda-adjunct: "npm:^5.1.0" - checksum: 10/ea69e017cf266b44e2ef8e01a862975789bc62a537ab28baffb80b51d99640e35fac44eea5b744e98d0f6151fa290a09f840a5ad3f0103ead508e628b10dbc7b + checksum: 10/f85d56c07191e26f3ca928d610efa813e5f9fa447d4732bc737fcab1c1b744047bf8baf83d094ad044d54872f99e2f6eceb94955fcecdecabc3844df35822227 languageName: node linkType: hard "swagger-ui-react@npm:^5.27.1": - version: 5.31.0 - resolution: "swagger-ui-react@npm:5.31.0" + version: 5.32.0 + resolution: "swagger-ui-react@npm:5.32.0" dependencies: "@babel/runtime-corejs3": "npm:^7.27.1" "@scarf/scarf": "npm:=1.4.0" @@ -48239,7 +47749,7 @@ __metadata: reselect: "npm:^5.1.1" serialize-error: "npm:^8.1.0" sha.js: "npm:^2.4.12" - swagger-client: "npm:^3.36.0" + swagger-client: "npm:^3.37.0" url-parse: "npm:^1.5.10" xml: "npm:=1.0.1" xml-but-prettier: "npm:^1.0.1" @@ -48247,31 +47757,31 @@ __metadata: peerDependencies: react: ">=16.8.0 <20" react-dom: ">=16.8.0 <20" - checksum: 10/328a531cb12639b1a72eedc5ab9532b9cbbf1ae2944b12476475c3263d1b9fee142c9ad5f44a610608689b05a91cc3456d129caa46235cef2fe16a0fb94869b4 + checksum: 10/7ac4481d452a54f1b656719be5cea4717afdee45b7367c6086eb278742d8cc331ad393db5668921c02934c9b95ff3d8ba6bb5f86101f5f9f593f84d798bc838b languageName: node linkType: hard "swc-loader@npm:^0.2.3": - version: 0.2.6 - resolution: "swc-loader@npm:0.2.6" + version: 0.2.7 + resolution: "swc-loader@npm:0.2.7" dependencies: "@swc/counter": "npm:^0.1.3" peerDependencies: "@swc/core": ^1.2.147 webpack: ">=2" - checksum: 10/fe90948c02a51bb8ffcff1ce3590e01dc12860b0bb7c9e22052b14fa846ed437781ae265614a5e14344bea22001108780f00a6e350e28c0b3499bc4cd11335fb + checksum: 10/15cbc3769209f7e2f927e49c19a5ef30fe03663bd34688289c003d966c7dfe782eec39de77c454aa7465ce84138d8bc31257b736b32ff3be10191f1de88cbbf5 languageName: node linkType: hard "swr@npm:^2.0.0, swr@npm:^2.2.5": - version: 2.4.0 - resolution: "swr@npm:2.4.0" + version: 2.4.1 + resolution: "swr@npm:2.4.1" dependencies: dequal: "npm:^2.0.3" use-sync-external-store: "npm:^1.6.0" peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/3536db25748b49659d87450ed40e391b0a8c2cfe387e41224ee505a18ae739477bd60caba1d8ff7bd961db923cf3d6771e4aee13de822855436a7bca9e343ccc + checksum: 10/611c0a39d42abb7d7eae37eb36e6a30a8a2c387bab6026d8db13ae41b9f8f3c2a187486008781de3f083ef5b39e6d90af1f275a73ade882a567e338166266356 languageName: node linkType: hard @@ -48289,6 +47799,17 @@ __metadata: languageName: node linkType: hard +"sync-fetch@npm:0.6.0": + version: 0.6.0 + resolution: "sync-fetch@npm:0.6.0" + dependencies: + node-fetch: "npm:^3.3.2" + timeout-signal: "npm:^2.0.0" + whatwg-mimetype: "npm:^4.0.0" + checksum: 10/524f4d45e903d208351d9bc43802002394cbf5e12699fb57471594f433cef5dfaf0c41e7f3484255865683f64125aeb6c279d264be123fc93bb9c6ae3f064bb4 + languageName: node + linkType: hard + "synckit@npm:^0.11.8": version: 0.11.11 resolution: "synckit@npm:0.11.11" @@ -48380,15 +47901,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3, tar@npm:^7.5.6": - version: 7.5.7 - resolution: "tar@npm:7.5.7" + version: 7.5.11 + resolution: "tar@npm:7.5.11" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21 + checksum: 10/fb2e77ee858a73936c68e066f4a602d428d6f812e6da0cc1e14a41f99498e4f7fd3535e355fa15157240a5538aa416026cfa6306bb0d1d1c1abf314b1f878e9a languageName: node linkType: hard @@ -48489,14 +48010,20 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.16": - version: 5.3.16 - resolution: "terser-webpack-plugin@npm:5.3.16" +"terminal-columns@npm:^2.0.0": + version: 2.0.0 + resolution: "terminal-columns@npm:2.0.0" + checksum: 10/f958993886e09d7c0780f47833316532a0c7dd7db2fb43da9d34a88c821633408a6e7084af59f99c20b045776814748f14a8e33573886186be7a30174092964f + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.3.17": + version: 5.3.17 + resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.25" jest-worker: "npm:^27.4.5" schema-utils: "npm:^4.3.0" - serialize-javascript: "npm:^6.0.2" terser: "npm:^5.31.1" peerDependencies: webpack: ^5.1.0 @@ -48507,7 +48034,7 @@ __metadata: optional: true uglify-js: optional: true - checksum: 10/09dfbff602acfa114cdd174254b69a04adbc47856021ab351e37982202fd1ec85e0b62ffd5864c98beb8e96aef2f43da490b3448b4541db539c2cff6607394a6 + checksum: 10/e51b00fe5e54beff82e8c2bea72b5104a2608e375d612ee56887e521210f840f2297fda2e0778c26bbd726b3c77ba2ec3234b2bdaa20b2f9174733745a457b33 languageName: node linkType: hard @@ -48537,25 +48064,25 @@ __metadata: linkType: hard "testcontainers@npm:^11.9.0": - version: 11.11.0 - resolution: "testcontainers@npm:11.11.0" + version: 11.12.0 + resolution: "testcontainers@npm:11.12.0" dependencies: "@balena/dockerignore": "npm:^1.0.2" - "@types/dockerode": "npm:^3.3.47" + "@types/dockerode": "npm:^4.0.1" archiver: "npm:^7.0.1" async-lock: "npm:^1.4.1" byline: "npm:^5.0.0" debug: "npm:^4.4.3" - docker-compose: "npm:^1.3.0" + docker-compose: "npm:^1.3.1" dockerode: "npm:^4.0.9" get-port: "npm:^7.1.0" proper-lockfile: "npm:^4.1.2" - properties-reader: "npm:^2.3.0" + properties-reader: "npm:^3.0.1" ssh-remote-port-forward: "npm:^1.0.4" tar-fs: "npm:^3.1.1" tmp: "npm:^0.2.5" - undici: "npm:^7.16.0" - checksum: 10/6a05baf5cd9ff0eab7ecb6c31f10e1204d9b4a88698f2ecea9f4008a6ca9130302a83f0b79e303bfda577de3dd26c74689bb4636720b28d7dd3753ce50422865 + undici: "npm:^7.22.0" + checksum: 10/d15ebc0be1c09192c4c26060bfec2f7ad2cf338624bed4a1ca2d548419853e54ea7deb8cd21cca21d55976552f09a00093ab7be822017e045f4c29e09b7e9ba2 languageName: node linkType: hard @@ -48660,6 +48187,13 @@ __metadata: languageName: node linkType: hard +"timeout-signal@npm:^2.0.0": + version: 2.0.0 + resolution: "timeout-signal@npm:2.0.0" + checksum: 10/5f022c225bac6542716478edf7bb5fc871d985cfa398b4f2eaa3d13fa6fda1225ce77cc65c5a92ae23b58882e2c14b83532a7c6f2f7710d06c9605b48ece4fe2 + languageName: node + linkType: hard + "timers-browserify@npm:^2.0.4": version: 2.0.11 resolution: "timers-browserify@npm:2.0.11" @@ -48710,6 +48244,13 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -49044,15 +48585,12 @@ __metadata: linkType: hard "ts-checker-rspack-plugin@npm:^1.1.5": - version: 1.2.6 - resolution: "ts-checker-rspack-plugin@npm:1.2.6" + version: 1.3.0 + resolution: "ts-checker-rspack-plugin@npm:1.3.0" dependencies: - "@babel/code-frame": "npm:^7.27.1" "@rspack/lite-tapable": "npm:^1.1.0" chokidar: "npm:^3.6.0" - is-glob: "npm:^4.0.3" - memfs: "npm:^4.51.1" - minimatch: "npm:^9.0.5" + memfs: "npm:^4.56.10" picocolors: "npm:^1.1.1" peerDependencies: "@rspack/core": ^1.0.0 || ^2.0.0-0 @@ -49060,7 +48598,7 @@ __metadata: peerDependenciesMeta: "@rspack/core": optional: true - checksum: 10/fc7af29af284caed8adedb766bbd289168f8ec0f45bbc8add9ba15900d143b8149d9ca197d7747bc54a79a052b7a24cb2c447141c8eab16ec4a80bc475d5e75a + checksum: 10/dfd19f2c210b8c6ee2255a6a6c7db30bcff08cb4664982f3c3edfc21ee78017b25ad1ce074e1a5ee9f7551291d4302debc53856d24dfcef1ca26209a9d997c18 languageName: node linkType: hard @@ -49212,7 +48750,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.13.0, tslib@npm:^1.14.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": +"tslib@npm:^1.14.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb @@ -49292,15 +48830,6 @@ __metadata: languageName: node linkType: hard -"type-check@npm:~0.3.2": - version: 0.3.2 - resolution: "type-check@npm:0.3.2" - dependencies: - prelude-ls: "npm:~1.1.2" - checksum: 10/11dec0b50d7c3fd2e630b4b074ba36918ed2b1efbc87dfbd40ba9429d49c58d12dad5c415ece69fcf358fa083f33466fc370f23ab91aa63295c45d38b3a60dda - languageName: node - linkType: hard - "type-detect@npm:4.0.8, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" @@ -49357,6 +48886,13 @@ __metadata: languageName: node linkType: hard +"type-flag@npm:^4.1.0": + version: 4.1.0 + resolution: "type-flag@npm:4.1.0" + checksum: 10/d3af106dab651751426f778095b3963791d15482b2154f92f4f98e5d6b72d593ae9873120d2de24f34191ab42c0d71c0259743162e543aee8d55b528ed1c6e78 + languageName: node + linkType: hard + "type-is@npm:^1.6.18, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -49613,13 +49149,6 @@ __metadata: languageName: node linkType: hard -"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5": - version: 1.0.6 - resolution: "uc.micro@npm:1.0.6" - checksum: 10/6898bb556319a38e9cf175e3628689347bd26fec15fc6b29fa38e0045af63075ff3fea4cf1fdba9db46c9f0cbf07f2348cd8844889dd31ebd288c29fe0d27e7a - languageName: node - linkType: hard - "uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": version: 2.1.0 resolution: "uc.micro@npm:2.1.0" @@ -49675,6 +49204,13 @@ __metadata: languageName: node linkType: hard +"unbash@npm:^2.2.0": + version: 2.2.0 + resolution: "unbash@npm:2.2.0" + checksum: 10/dc169c6cacff0364c3d46dbe3f08f4c812cee63359f94ebace483b1a382585d5a1bf4007c12ed3c73671a0256d0026efc23e4732583ae76b42d8570fc4680667 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -49694,17 +49230,10 @@ __metadata: languageName: node linkType: hard -"underscore@npm:1.12.1": - version: 1.12.1 - resolution: "underscore@npm:1.12.1" - checksum: 10/c3bb50c61ef7dea4b9add6d48f98316f65de1075801dc8cd01ecd82122b99eb17ce0f068f00f6cc10245d5cf75c443ce96f8efc5458d7773b44af9bd7c2ccc56 - languageName: node - linkType: hard - "underscore@npm:^1.12.1, underscore@npm:^1.13.3": - version: 1.13.7 - resolution: "underscore@npm:1.13.7" - checksum: 10/1ce3368dbe73d1e99678fa5d341a9682bd27316032ad2de7883901918f0f5d50e80320ccc543f53c1862ab057a818abc560462b5f83578afe2dd8dd7f779766c + version: 1.13.8 + resolution: "underscore@npm:1.13.8" + checksum: 10/b50ac5806d059cc180b1bd9adea6f7ed500021f4dc782dfc75d66a90337f6f0506623c1b37863f4a9bf64ffbeb5769b638a54b7f2f5966816189955815953139 languageName: node linkType: hard @@ -49738,10 +49267,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:^7.16.0, undici@npm:^7.2.3": - version: 7.20.0 - resolution: "undici@npm:7.20.0" - checksum: 10/09ca3e1255cf05f3c76e6dff2ae760131ea5bba57290b9b184bd94f5167939548e7ea73292c524c25eb91f5a2152623394d4c6124e222d34fcd53ef733c6b156 +"undici@npm:^7.2.3, undici@npm:^7.22.0": + version: 7.24.4 + resolution: "undici@npm:7.24.4" + checksum: 10/747e76e0fd685ae1bb6fc1a2ebce0caca4ee8bd5599a77da36a3f94eac146987a9547bdbec7a74d18c0776df8ad348dccb4209901ca83fc4076f560de0d5dc7a languageName: node linkType: hard @@ -49754,13 +49283,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" @@ -50113,6 +49635,15 @@ __metadata: languageName: node linkType: hard +"upper-case@npm:^2.0.2": + version: 2.0.2 + resolution: "upper-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/508723a2b03ab90cf1d6b7e0397513980fab821cbe79c87341d0e96cedefadf0d85f9d71eac24ab23f526a041d585a575cfca120a9f920e44eb4f8a7cf89121c + languageName: node + linkType: hard + "uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -50207,6 +49738,13 @@ __metadata: languageName: node linkType: hard +"urlpattern-polyfill@npm:^10.0.0": + version: 10.1.0 + resolution: "urlpattern-polyfill@npm:10.1.0" + checksum: 10/a3a5cb577f40e9d14a2bc0e23e540d088ec8c18e52932337c2499a8118f32aa8fa32578f0cddaa189de671964b0f33ba7e008d0784b99804a09a9ff924f87f5a + languageName: node + linkType: hard + "urlpattern-polyfill@npm:^8.0.0": version: 8.0.2 resolution: "urlpattern-polyfill@npm:8.0.2" @@ -50214,25 +49752,18 @@ __metadata: languageName: node linkType: hard -"urlpattern-polyfill@npm:^9.0.0": - version: 9.0.0 - resolution: "urlpattern-polyfill@npm:9.0.0" - checksum: 10/63d59e08d58189d340e3acb0fb69c11d8f06da5e38c091cdac66cac07e4ca81378ad19cd1a923d5593a899603a0e607fe3ef793ef368fefbc1b2b840b24839b8 - languageName: node - linkType: hard - -"use-callback-ref@npm:^1.3.0": - version: 1.3.0 - resolution: "use-callback-ref@npm:1.3.0" +"use-callback-ref@npm:^1.3.3": + version: 1.3.3 + resolution: "use-callback-ref@npm:1.3.3" dependencies: tslib: "npm:^2.0.0" peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/f9f1b217db60419b033228ba17cee5c521123e7c7f35577258a1abdce6d9623e5880f0bed3a0504eff35fdf6c761a2b2e020337a34218fb86229b8641772654a + checksum: 10/adf06a7b6a27d3651c325ac9b66d2b82ccacaed7450b85b211d123e91d9a23cb5a587fcc6db5b4fd07ac7233e5abf024d30cf02ddc2ec46bca712151c0836151 languageName: node linkType: hard @@ -50302,19 +49833,19 @@ __metadata: languageName: node linkType: hard -"use-sidecar@npm:^1.1.2": - version: 1.1.2 - resolution: "use-sidecar@npm:1.1.2" +"use-sidecar@npm:^1.1.3": + version: 1.1.3 + resolution: "use-sidecar@npm:1.1.3" dependencies: detect-node-es: "npm:^1.1.0" tslib: "npm:^2.0.0" peerDependencies: - "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 10/ec99e31aefeb880f6dc4d02cb19a01d123364954f857811470ece32872f70d6c3eadbe4d073770706a9b7db6136f2a9fbf1bb803e07fbb21e936a47479281690 + checksum: 10/2fec05eb851cdfc4a4657b1dfb434e686f346c3265ffc9db8a974bb58f8128bd4a708a3cc00e8f51655fccf81822ed4419ebed42f41610589e3aab0cf2492edb languageName: node linkType: hard @@ -50520,13 +50051,6 @@ __metadata: languageName: node linkType: hard -"value-or-promise@npm:^1.0.11, value-or-promise@npm:^1.0.12": - version: 1.0.12 - resolution: "value-or-promise@npm:1.0.12" - checksum: 10/a4cc31fc9c3826b8a216ef2037b676904324c00c4acd903aaec2fe0c08516a189345261dd3cc822ec108532b2ea36b7c99bbdee1c3ddcb7f4b3d57d7e61b2064 - languageName: node - linkType: hard - "vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -50727,13 +50251,13 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:^2.4.4": - version: 2.4.4 - resolution: "watchpack@npm:2.4.4" +"watchpack@npm:^2.5.1": + version: 2.5.1 + resolution: "watchpack@npm:2.5.1" dependencies: glob-to-regexp: "npm:^0.4.1" graceful-fs: "npm:^4.1.2" - checksum: 10/cfa3473fc12a1a1b88123056941e90c462a67aedc10b242229eeeccdd45ed0b763c3b591caaffb0f7d77295b539b5518bb1ad3bcd891ae6505dfeae4cf51fd15 + checksum: 10/9c9cdd4a9f9ae146b10d15387f383f52589e4cc27b324da6be8e7e3e755255b062a69dd7f00eef2ce67b2c01e546aae353456e74f8c1350bba00462cc6375549 languageName: node linkType: hard @@ -50891,10 +50415,10 @@ __metadata: languageName: node linkType: hard -"webpack-sources@npm:^3.3.3": - version: 3.3.3 - resolution: "webpack-sources@npm:3.3.3" - checksum: 10/ec5d72607e8068467370abccbfff855c596c098baedbe9d198a557ccf198e8546a322836a6f74241492576adba06100286592993a62b63196832cdb53c8bae91 +"webpack-sources@npm:^3.3.4": + version: 3.3.4 + resolution: "webpack-sources@npm:3.3.4" + checksum: 10/714427b235b04c2d7cf229f204b9e65145ea3643da3c7b139ebfa8a51056238d1e3a2a47c3cc3fc8eab71ed4300f66405cdc7cff29cd2f7f6b71086252f81cf1 languageName: node linkType: hard @@ -50905,9 +50429,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:~5.104.0": - version: 5.104.1 - resolution: "webpack@npm:5.104.1" +"webpack@npm:^5, webpack@npm:~5.105.0": + version: 5.105.4 + resolution: "webpack@npm:5.105.4" dependencies: "@types/eslint-scope": "npm:^3.7.7" "@types/estree": "npm:^1.0.8" @@ -50915,11 +50439,11 @@ __metadata: "@webassemblyjs/ast": "npm:^1.14.1" "@webassemblyjs/wasm-edit": "npm:^1.14.1" "@webassemblyjs/wasm-parser": "npm:^1.14.1" - acorn: "npm:^8.15.0" + acorn: "npm:^8.16.0" acorn-import-phases: "npm:^1.0.3" browserslist: "npm:^4.28.1" chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.17.4" + enhanced-resolve: "npm:^5.20.0" es-module-lexer: "npm:^2.0.0" eslint-scope: "npm:5.1.1" events: "npm:^3.2.0" @@ -50931,15 +50455,15 @@ __metadata: neo-async: "npm:^2.6.2" schema-utils: "npm:^4.3.3" tapable: "npm:^2.3.0" - terser-webpack-plugin: "npm:^5.3.16" - watchpack: "npm:^2.4.4" - webpack-sources: "npm:^3.3.3" + terser-webpack-plugin: "npm:^5.3.17" + watchpack: "npm:^2.5.1" + webpack-sources: "npm:^3.3.4" peerDependenciesMeta: webpack-cli: optional: true bin: webpack: bin/webpack.js - checksum: 10/c22b58fdf41d993971516154cb621d13b7b61dd744b7e2c6be972e9afcc9942694a8aaa322c07fba46448c69d663023ff53d0aeeb00eb0b125d83ace5ca8fa3c + checksum: 10/ae8088dd1c995fa17b920009f864138297a9ea5089bc563601f661fa4a31bb24b000cc91ae122168ce9def79c49258b8aa1021c2754c3555205c29a0d6c9cc8d languageName: node linkType: hard @@ -51186,13 +50710,6 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:~1.2.3": - version: 1.2.4 - resolution: "word-wrap@npm:1.2.4" - checksum: 10/a749c0cf410724acde4bdb263dcb13de61489dde22889a6a408e8a57e5948477c5b7438a757e25bb92985ed02562ab271aade90d605a24f3ae78410b638fbbd8 - languageName: node - linkType: hard - "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -51283,7 +50800,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.18.3, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.18.3, ws@npm:^8.19.0, ws@npm:^8.8.0": version: 8.19.0 resolution: "ws@npm:8.19.0" peerDependencies: @@ -51445,7 +50962,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:^2.7.0, yaml@npm:^2.8.1": +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:^2.7.0, yaml@npm:^2.8.1, yaml@npm:^2.8.2": version: 2.8.2 resolution: "yaml@npm:2.8.2" bin: @@ -51516,18 +51033,18 @@ __metadata: fs-extra: "npm:^11.2.0" nodemon: "npm:^3.0.1" semver: "npm:^7.6.0" - snyk-nodejs-lockfile-parser: "npm:^1.58.14" + snyk-nodejs-lockfile-parser: "npm:^2.0.0" yaml: "npm:^2.0.0" languageName: unknown linkType: soft "yauzl@npm:^3.0.0": - version: 3.2.0 - resolution: "yauzl@npm:3.2.0" + version: 3.2.1 + resolution: "yauzl@npm:3.2.1" dependencies: buffer-crc32: "npm:~0.2.3" pend: "npm:~1.2.0" - checksum: 10/a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b + checksum: 10/15dfae75fbfe59c6a1b7a2cb27a995cda0ee70549d32d6b19937e84897436170f169f6bbefc34b9e9beb9c9114a1b8a8a40e7687a907909a19681ebcbf35a1f3 languageName: node linkType: hard